Postbox -> TelegramEngine waves 27-36

Consumer-sweep, facade-addition, and Peer→EnginePeer migrations:

- Wave 27: preferencesView consumer sweep
- Wave 28: resourceData consumer sweep
- Wave 29: resourceStatus consumer sweep
- Wave 30: _asStatus() bridge cleanup
- Wave 31: unused-import sweep re-run
- Wave 32: resourceStatus residue sweep
- Wave 33: loadedPeerWithId consumer sweep
- Wave 34: FoundPeer.peer Peer -> EnginePeer
- Wave 35: SendAsPeer.peer Peer -> EnginePeer
- Wave 36: ContactListPeer.peer Peer -> EnginePeer

Also includes per-wave specs, implementation plans, outcome logs, and
a CLAUDE.md wave-counter update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-04-22 21:06:25 +04:00
parent 9187fbb6db
commit 8408e0ae19
130 changed files with 4696 additions and 524 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-21): 26 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): 36 waves plus standalone cleanups. See the log file for per-wave detail; the list of still-open migration opportunities lives in the `project_postbox_refactor_next_wave.md` memory file.
### Rules that apply to every wave
@ -120,14 +120,15 @@ Distilled lessons from waves 126. Each bullet below has a full-form counterpa
Full per-shape recipe and wave-specific examples in the log.
### TelegramEngine.Resources facade inventory (as of wave 26)
### TelegramEngine.Resources facade inventory (as of wave 32)
All mediaBox methods with clean signatures (no Postbox-protocol leaks, no complex return-type migrations) have been migrated to `TelegramEngine.Resources`. Quick reference for consumers — all of these live in `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift`:
| Facade | Wave | Wraps |
|---|---|---|
| `fetch(reference:userLocation:userContentType:)` | 3 | `fetchedMediaResource` |
| `status(resource:)` | 3 | `MediaBox.resourceStatus` |
| `status(resource:)` | 3 | `MediaBox.resourceStatus` (resource-based) |
| `status(id:, resourceSize:)` | 32 | `MediaBox.resourceStatus(_ id:, resourceSize:)` |
| `data(resource:, pathExtension:, waitUntilFetchStatus:)` | 3 | `MediaBox.resourceData` (resource-based) |
| `data(id:, attemptSynchronously:)` | 3 | `MediaBox.resourceData` (id-based, defaults to `.complete(waitUntilFetchStatus: false)`) |
| `custom(id:, fetch:, cacheTimeout:, attemptSynchronously:)` | pre-wave-21 | `MediaBox.customResourceData` |

View file

@ -0,0 +1,944 @@
# Wave 36: `ContactListPeer.peer: Peer → EnginePeer` Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate the public enum case `ContactListPeer.peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum in a single atomic commit. Cascading changes: change `ContactListPeer.indexName` return type from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` (drops 2 `EnginePeer.IndexName(...)` wraps at one call site); rewrite the enum's custom `==` to use `EnginePeer`'s synthesized Equatable; drop 20 outflow `._asPeer()` bridges, 16 inflow `EnginePeer(peer)` wraps; rewrite 2 Postbox-concrete cast chains to EnginePeer case patterns.
**Architecture:** One atomic commit. The enum-case payload change is necessarily atomic. `ContactListPeer` lives in `submodules/AccountContext/Sources/ContactSelectionController.swift`; 7 consumer files touched in addition. 2 consumer files verified untouched (`ComposeController.swift`, `ChatSendAudioMessageContextPreview.swift`). No new wrappers, no new typealiases. `import Postbox` stays in every touched consumer (follow-up unused-import sweep handles it).
**Tech Stack:** Swift, Bazel build via Make.py wrapper. No tests — verification is build success + targeted grep checks.
**Spec:** `docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md`
---
## File Structure
**Modified files (8 expected — 1 definition + 7 consumer. Plus 2 verify-only.)**
| File | Edits | Categories |
|---|---|---|
| `submodules/AccountContext/Sources/ContactSelectionController.swift` | 3 (case type + indexName return type + `==` body) | α |
| `submodules/ContactListUI/Sources/ContactListNode.swift` | ~21 (12 outflow + 4 inflow + 2 cast rewrites [L182-186, L1968] + 2 IndexName wraps [L517]) | β + δ + φ + ε′ |
| `submodules/ContactListUI/Sources/ContactsController.swift` | 1 (inflow wrap at L294) | δ |
| `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift` | 7 (3 outflow + 4 inflow) | β + δ |
| `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` | 6 (2 outflow + 4 inflow) | β + δ |
| `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift` | 2 (1 outflow + 1 inflow) | β + δ |
| `submodules/TelegramUI/Sources/ContactSelectionController.swift` | 2 (inflow wraps L517/527) | δ |
| `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift` | 2 (outflow bridges L160/230) | β |
**Verify-only (no edits expected):**
| File | Reason |
|---|---|
| `submodules/TelegramUI/Sources/ComposeController.swift` | Destructures at L120/160 access `.id` only. Same-type access works on EnginePeer. |
| `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift` | Only holds `[ContactListPeer]` at collection level; no `.peer` destructures. |
**EnginePeer enum case mapping (used in cast rewrites):**
| Postbox concrete | EnginePeer case |
|---|---|
| `TelegramUser` | `.user(TelegramUser)` |
| `TelegramGroup` | `.legacyGroup(TelegramGroup)` |
| `TelegramChannel` | `.channel(TelegramChannel)` |
**Sites that stay as `._asPeer()` bridges (NOT in wave scope):**
- `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift:488, 528, 562``canSendMessagesToPeer(peer._asPeer())` / `canSendMessagesToPeer(peer.peer._asPeer())`. `canSendMessagesToPeer(_: Peer)` migration is a deferred future wave.
- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift:171, 201, 748``peerTokenTitle(accountPeerId:..., peer: peer._asPeer(), strings:...)`. `peerTokenTitle(peer: Peer)` migration is out of scope.
---
## Task 1: Edit `AccountContext/Sources/ContactSelectionController.swift` — definition
**Files:**
- Modify: `submodules/AccountContext/Sources/ContactSelectionController.swift`
Foundational change. Without it, none of the consumer edits compile.
- [ ] **Step 1.1: Update the case payload type, `indexName` return type, and `==` operator body**
Edit using the Edit tool:
```swift
// OLD (lines 61-99)
public enum ContactListPeer: Equatable {
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId {
switch self {
case let .peer(peer, _, _):
return .peer(peer.id)
case let .deviceContact(id, _):
return .deviceContact(id)
}
}
public var indexName: PeerIndexNameRepresentation {
switch self {
case let .peer(peer, _, _):
return peer.indexName
case let .deviceContact(_, contact):
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
}
}
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else {
return false
}
case let .deviceContact(id, contact):
if case .deviceContact(id, contact) = rhs {
return true
} else {
return false
}
}
}
}
```
```swift
// NEW
public enum ContactListPeer: Equatable {
case peer(peer: EnginePeer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId {
switch self {
case let .peer(peer, _, _):
return .peer(peer.id)
case let .deviceContact(id, _):
return .deviceContact(id)
}
}
public var indexName: EnginePeer.IndexName {
switch self {
case let .peer(peer, _, _):
return peer.indexName
case let .deviceContact(_, contact):
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
}
}
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer == rhsPeer, lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else {
return false
}
case let .deviceContact(id, contact):
if case .deviceContact(id, contact) = rhs {
return true
} else {
return false
}
}
}
}
```
Three changes in this edit:
1. Line 62: `peer: Peer``peer: EnginePeer`
2. Line 74: return type `PeerIndexNameRepresentation``EnginePeer.IndexName`
3. Line 86 (inside the `==` operator): `lhsPeer.isEqual(rhsPeer)``lhsPeer == rhsPeer`
`EnginePeer.IndexName.personName(first:last:addressNames:phoneNumber:)` has the same labels/types as `PeerIndexNameRepresentation.personName`, so line 79 body is untouched — only its return target enum changes.
- [ ] **Step 1.2: Verify**
Run:
```bash
grep -nE "case peer\(peer:|public var indexName:|\.isEqual\(" submodules/AccountContext/Sources/ContactSelectionController.swift
```
Expected output:
- Line 62: `case peer(peer: EnginePeer, ...)`
- Line 74: `public var indexName: EnginePeer.IndexName {`
- No `isEqual(` match on the `==` path (the only remaining occurrences would be unrelated).
Do not commit yet.
---
## Task 2: Edit `ContactListNode.swift` — largest consumer, multi-category
**Files:**
- Modify: `submodules/ContactListUI/Sources/ContactListNode.swift`
Most changes happen here: 12 outflow bridges + 4 inflow wraps + 2 cast chain rewrites + 2 IndexName wrap drops.
- [ ] **Step 2.1: Drop the 12 outflow `._asPeer()` bridges via `replace_all`**
All 12 `._asPeer()` bridges at ContactListPeer.peer construction sites follow the shape `._asPeer(), isGlobal:`. Non-construction `._asPeer()` uses in this file (if any) feed other functions and do NOT use this exact substring.
Pre-flight verify:
```bash
grep -cE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactListNode.swift
```
Expected: `12`.
If the count is 12, apply the Edit tool with `replace_all=true`:
- `old_string`: `._asPeer(), isGlobal:`
- `new_string`: `, isGlobal:`
If the count is not 12, fall back to per-site Edits at lines 632, 690, 701, 747, 765, 1365, 1647, 1656, 1693, 1731, 1942, 1944 using enough surrounding context to make each `old_string` unique.
- [ ] **Step 2.2: Verify the 12 outflow drops**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactListNode.swift
```
Expected: zero matches.
- [ ] **Step 2.3: Drop 2 inflow wraps at L204**
Read lines 200210 first to confirm the line text.
Edit:
```swift
// OLD (line 204)
itemPeer = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))
```
```swift
// NEW
itemPeer = .peer(peer: peer, chatPeer: peer)
```
- [ ] **Step 2.4: Drop 1 inflow wrap at L252**
Read lines 248256 first to confirm.
Edit:
```swift
// OLD (line 252)
interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
```
```swift
// NEW
interaction.openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic)
```
- [ ] **Step 2.5: Drop 1 inflow wrap at L844**
Read lines 840848 first to confirm.
Edit:
```swift
// OLD (line 844)
if let isPeerEnabled, !isPeerEnabled(EnginePeer(peer)) {
```
```swift
// NEW
if let isPeerEnabled, !isPeerEnabled(peer) {
```
- [ ] **Step 2.6: Rewrite the L182-186 cast chain to EnginePeer case patterns**
Read lines 176200 first. The cast chain is inside the ContactListPeer.peer destructure at line 177.
Edit:
```swift
// OLD (lines 182-186)
} else {
if let _ = peer as? TelegramUser {
status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat)
} else if let group = peer as? TelegramGroup {
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil)
} else if let channel = peer as? TelegramChannel {
```
```swift
// NEW
} else {
if case .user = peer {
status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat)
} else if case let .legacyGroup(group) = peer {
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil)
} else if case let .channel(channel) = peer {
```
`channel.info` access inside the surviving inner block continues to compile unchanged (`EnginePeer.channel` wraps `TelegramChannel`). `group.participantCount` inside the `legacyGroup` branch works identically. The first branch doesn't bind the user — the `case .user = peer` form preserves that.
- [ ] **Step 2.7: Rewrite the L1968 cast to an EnginePeer case pattern**
Read lines 19641976 first. The cast is inside the ContactListPeer.peer destructure at line 1966.
Edit:
```swift
// OLD (lines 1967-1968)
if requirePhoneNumbers,
let user = peer as? TelegramUser {
```
```swift
// NEW
if requirePhoneNumbers,
case let .user(user) = peer {
```
`user.phone` on the following line continues to compile (`EnginePeer.user` wraps `TelegramUser`).
- [ ] **Step 2.8: Drop 2 IndexName wraps at L517**
Read lines 515522 first.
Edit:
```swift
// OLD (line 517)
let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder)
```
```swift
// NEW
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder)
```
`ContactListPeer.indexName` now returns `EnginePeer.IndexName` (from Task 1), and `isLessThan(other:ordering:)` is defined on `EnginePeer.IndexName` at `submodules/LocalizedPeerData/Sources/PeerTitle.swift:64`, so the wrap idiom is no longer required.
- [ ] **Step 2.9: Verify ContactListNode.swift changes**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)|peer as\? Telegram(User|Group|Channel)\b|EnginePeer\.IndexName\(lhs\.indexName\)|EnginePeer\.IndexName\(rhs\.indexName\)" submodules/ContactListUI/Sources/ContactListNode.swift
```
Expected output: only `EnginePeer(peer)` matches at lines 1819 and 1825 (out-of-scope; `peer` there is from `entryData.renderedPeer.peer`, raw `Peer`, wraps stay). Similarly, `peer as? TelegramChannel` at 1802/1820 and `peer is TelegramGroup` at 1818 stay.
If any other match appears, re-examine that site and apply the matching fix.
---
## Task 3: Edit `ContactsController.swift` — 1 inflow wrap drop
**Files:**
- Modify: `submodules/ContactListUI/Sources/ContactsController.swift`
- [ ] **Step 3.1: Drop inflow wrap at L294**
Read lines 285300 first.
Edit:
```swift
// OLD (line 294)
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), purposefulAction: { [weak self] in
```
```swift
// NEW
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), purposefulAction: { [weak self] in
```
`peer` here is destructured from the ContactListPeer.peer case at line 287; post-migration it is already `EnginePeer`. `chatLocation: .peer(EnginePeer)` case takes `EnginePeer`.
- [ ] **Step 3.2: Verify**
Run:
```bash
grep -nE "chatLocation: \.peer\(EnginePeer\(peer\)\)" submodules/ContactListUI/Sources/ContactsController.swift
```
Expected: zero matches.
---
## Task 4: Edit `ContactsSearchContainerNode.swift` — 3 outflow + 4 inflow
**Files:**
- Modify: `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift`
- [ ] **Step 4.1: Drop the 3 outflow `._asPeer()` bridges at L494/535/569**
Use the same `._asPeer(), isGlobal:` pattern as Task 2.1. The 3 bridges at `ContactListPeer.peer(...)` constructions all match this substring; the 3 unrelated bridges at L488/528/562 (`canSendMessagesToPeer(...)` sites) do NOT match (they lack the `, isGlobal:` suffix).
Pre-flight verify:
```bash
grep -cE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift
```
Expected: `3`.
Apply Edit with `replace_all=true`:
- `old_string`: `._asPeer(), isGlobal:`
- `new_string`: `, isGlobal:`
- [ ] **Step 4.2: Drop 4 inflow wraps at L164/165/181**
Read lines 160185 first.
Three edits, each targeting one source line.
Edit (line 164 — 2 wraps in one expression):
```swift
// OLD
peerItem = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))
```
```swift
// NEW
peerItem = .peer(peer: peer, chatPeer: peer)
```
Edit (line 165):
```swift
// OLD
nativePeer = EnginePeer(peer)
```
```swift
// NEW
nativePeer = peer
```
Edit (line 181):
```swift
// OLD
openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
```
```swift
// NEW
openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic)
```
- [ ] **Step 4.3: Verify**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift
```
Expected: zero matches.
The `._asPeer()` calls at L488/528/562 (feeding `canSendMessagesToPeer`) should remain. Verify:
```bash
grep -nE "canSendMessagesToPeer\(.*\._asPeer\(\)\)" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift
```
Expected: 3 matches (L488, L528, L562).
---
## Task 5: Edit `TelegramUI/Sources/ContactMultiselectionController.swift` — 2 outflow + 4 inflow
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`
- [ ] **Step 5.1: Drop 2 outflow bridges at L451/459 via `replace_all`**
Pre-flight verify:
```bash
grep -cE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: `2`.
Apply Edit with `replace_all=true`:
- `old_string`: `._asPeer(), isGlobal:`
- `new_string`: `, isGlobal:`
Unrelated `._asPeer()` calls at L171/201/748 (feeding `peerTokenTitle(peer: Peer, ...)`) do NOT use this substring and stay.
- [ ] **Step 5.2: Drop 4 inflow wraps at L386/403/481/491**
Read the file around each site to confirm exact text. Two wraps (L386, L403) have identical text; the other two (L481, L491) have distinct tails.
Edit for L386 and L403 — `replace_all=true` on the substring:
Pre-flight verify:
```bash
grep -cE "subject: \.peer\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: `2`.
Apply Edit with `replace_all=true`:
- `old_string`: `subject: .peer(EnginePeer(peer))`
- `new_string`: `subject: .peer(peer)`
Edit for L481:
```swift
// OLD
self.params.sendMessage?(EnginePeer(peer))
```
```swift
// NEW
self.params.sendMessage?(peer)
```
Edit for L491:
```swift
// OLD
self.params.openProfile?(EnginePeer(peer))
```
```swift
// NEW
self.params.openProfile?(peer)
```
- [ ] **Step 5.3: Verify**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:|subject: \.peer\(EnginePeer\(peer\)\)|sendMessage\?\(EnginePeer\(peer\)\)|openProfile\?\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: zero matches.
Preserved bridge sites (sanity check):
```bash
grep -nE "peerTokenTitle\(.*\._asPeer\(\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: 3 matches (L171, L201, L748).
---
## Task 6: Edit `TelegramUI/Sources/ContactMultiselectionControllerNode.swift` — 1 outflow + 1 inflow
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift`
- [ ] **Step 6.1: Drop 1 outflow bridge at L317**
Read lines 315320 first.
Edit:
```swift
// OLD (line 317)
self?.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
```
```swift
// NEW
self?.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil))
```
- [ ] **Step 6.2: Drop 1 inflow wrap at L492**
Read lines 488495 first.
Edit:
```swift
// OLD (line 492)
callTitle = self.presentationData.strings.NewCall_ActionCallSingle(EnginePeer(peer).compactDisplayTitle).string
```
```swift
// NEW
callTitle = self.presentationData.strings.NewCall_ActionCallSingle(peer.compactDisplayTitle).string
```
- [ ] **Step 6.3: Verify**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)\.compactDisplayTitle" submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift
```
Expected: zero matches.
---
## Task 7: Edit `TelegramUI/Sources/ContactSelectionController.swift` — 2 inflow wraps
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactSelectionController.swift`
- [ ] **Step 7.1: Drop 2 inflow wraps at L517/527**
Read lines 510535 first. Both sites are inside the destructure at L504.
Edit for L517:
```swift
// OLD
self.sendMessage?(EnginePeer(peer))
```
```swift
// NEW
self.sendMessage?(peer)
```
Edit for L527:
```swift
// OLD
self.openProfile?(EnginePeer(peer))
```
```swift
// NEW
self.openProfile?(peer)
```
- [ ] **Step 7.2: Verify**
Run:
```bash
grep -nE "sendMessage\?\(EnginePeer\(peer\)\)|openProfile\?\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactSelectionController.swift
```
Expected: zero matches.
---
## Task 8: Edit `TelegramUI/Sources/ContactSelectionControllerNode.swift` — 2 outflow bridges
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift`
- [ ] **Step 8.1: Drop 2 outflow bridges at L160/230 via `replace_all`**
Pre-flight verify:
```bash
grep -cE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift
```
Expected: `2`.
Apply Edit with `replace_all=true`:
- `old_string`: `._asPeer(), isGlobal:`
- `new_string`: `, isGlobal:`
- [ ] **Step 8.2: Verify**
Run:
```bash
grep -nE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift
```
Expected: zero matches.
---
## Task 9: Verify no-edit consumer files
**Files (read only):**
- Read: `submodules/TelegramUI/Sources/ComposeController.swift`
- Read: `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift`
- [ ] **Step 9.1: Confirm ComposeController.swift has no inflow wraps, casts, or outflow bridges**
Run:
```bash
grep -nE "\.peer\(peer:|EnginePeer\(peer\)|peer as\? Telegram|\._asPeer\(\)" submodules/TelegramUI/Sources/ComposeController.swift
```
Expected: zero matches (destructures at L120/160 only access `.id`).
If any match appears, add the appropriate fix step here and re-run Task 9.1 before proceeding.
- [ ] **Step 9.2: Confirm ChatSendAudioMessageContextPreview.swift has no ContactListPeer.peer destructures**
Run:
```bash
grep -nE "case let \.peer\(peer, _, _\)|case \.peer\(let peer|EnginePeer\(peer\)|\.peer\(peer: " submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift
```
Expected: zero matches. The file only references `[ContactListPeer]` at the collection level.
---
## Task 10: Build verification (first pass)
- [ ] **Step 10.1: Run the full build with `--continueOnError`**
Run:
```bash
source ~/.zshrc 2>/dev/null && python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError 2>&1 | tee /tmp/wave36-build.log
```
Expected outcome: ideally clean. Realistic: 03 inventory-missed sites (wave 35 trend was 14% miss rate on a 7-file wave; this 8-file wave has a larger surface area, so budget for up to 3 misses).
- [ ] **Step 10.2: Triage build errors**
Likely patterns and fixes:
| Error | Fix |
|---|---|
| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer'` at a call site | Add `._asPeer()` bridge. The callee takes raw `Peer` and is out of wave scope. |
| `cannot convert value of type 'Peer' to expected argument type 'EnginePeer'` at a `.peer(peer:, ...)` construction | Wrap raw peer with `EnginePeer(...)`. The raw-Peer source is probably from `transaction.getPeer(...)` or similar. |
| `value of type 'EnginePeer' has no member 'isEqual'` | Replace with `==`. |
| `type 'EnginePeer' cannot be cast to 'TelegramUser'` / `TelegramGroup` / `TelegramChannel` | Missed φ-category cast — rewrite to `case .user = peer` / `case let .legacyGroup(x) = peer` / `case let .channel(x) = peer`. |
| `cannot invoke initializer for type 'EnginePeer' with an argument list of type '(EnginePeer)'` | Missed inflow drop — strip `EnginePeer(...)` wrap. |
| `cannot convert value of type 'EnginePeer.IndexName' to expected argument type 'PeerIndexNameRepresentation'` | Either wrap the call site's expected-type change or adjust the consumer to accept `EnginePeer.IndexName`. Probably rare — ContactListPeer.indexName consumers were grepped in pre-flight and found only in ContactListNode. |
| `value of type 'EnginePeer' has no member '<postbox-Peer-only method>'` | That method is only on the Postbox `Peer` protocol. Bridge via `._asPeer()` OR find the EnginePeer-native equivalent. |
For each error: identify file:line, apply the fix, re-run the build until clean.
- [ ] **Step 10.3: Iterate to clean build**
Re-run the build after each batch of fixes. The wave is complete when the build returns 0 errors for the targeted configuration.
If 10+ unexpected errors surface, halt and reassess: the inventory may have significantly undercounted and the wave may need to be split. Discuss with the user before continuing.
---
## Task 11: Post-build grep validations
- [ ] **Step 11.1: Outflow-bridge-drop validation**
Run:
```bash
grep -rnE "\.peer\(peer: \w+\._asPeer\(\), isGlobal:" submodules/ --include="*.swift"
```
Expected: zero hits. Any remaining site is a missed outflow-bridge drop.
- [ ] **Step 11.2: Inflow-wrap-drop validation**
Run:
```bash
for f in submodules/ContactListUI/Sources/ContactListNode.swift \
submodules/ContactListUI/Sources/ContactsController.swift \
submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift \
submodules/TelegramUI/Sources/ContactMultiselectionController.swift \
submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift \
submodules/TelegramUI/Sources/ContactSelectionController.swift; do
echo "=== $f ==="
grep -nE "EnginePeer\(peer\)" "$f"
done
```
Expected hits:
- ContactListNode.swift L1819, L1825 (raw `renderedPeer.peer`, out-of-scope wraps stay)
- Any other hit in the 6 listed files is a missed inflow drop — inspect and fix.
- [ ] **Step 11.3: Cast-rewrite validation**
Run:
```bash
grep -nE "\bpeer (as\?|as!|is) Telegram(User|Group|Channel)\b" submodules/ContactListUI/Sources/ContactListNode.swift
```
Expected: only L1802, L1818, L1820 remain (out-of-scope, `peer` is raw from `renderedPeer.peer`).
If L182, L184, L186, or L1968 appear, those are missed φ rewrites.
- [ ] **Step 11.4: IndexName wrap validation**
Run:
```bash
grep -nE "EnginePeer\.IndexName\(lhs\.indexName\)|EnginePeer\.IndexName\(rhs\.indexName\)" submodules/ContactListUI/Sources/ContactListNode.swift
```
Expected: zero matches.
- [ ] **Step 11.5: isEqual-in-==-operator validation**
Run:
```bash
grep -nE "lhsPeer\.isEqual\(rhsPeer\)" submodules/AccountContext/Sources/ContactSelectionController.swift
```
Expected: zero matches.
- [ ] **Step 11.6: Construction-site sanity sweep**
Run:
```bash
grep -rnE "ContactListPeer\.peer\(peer: |\.peer\(peer: \w+, isGlobal:" submodules/ --include="*.swift" | head -40
```
Inspect each hit. Expected forms:
- `.peer(peer: <EnginePeer-expr>, isGlobal: …)` where `<EnginePeer-expr>` is either a local already typed `EnginePeer` or `EnginePeer(<raw-Peer>)`.
- Anything of the form `.peer(peer: <raw-Peer>, isGlobal: …)` where `<raw-Peer>` is a Postbox `Peer` value is a miss (would surface as a build error — this is a belt-and-suspenders check).
If any validation fails, return to Task 10.
---
## Task 12: Atomic commit + memory + log update
- [ ] **Step 12.1: Stage and review**
Run:
```bash
git status --short
git diff --stat
```
Confirm exactly 8 modified Swift files:
- `submodules/AccountContext/Sources/ContactSelectionController.swift`
- `submodules/ContactListUI/Sources/ContactListNode.swift`
- `submodules/ContactListUI/Sources/ContactsController.swift`
- `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift`
- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`
- `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift`
- `submodules/TelegramUI/Sources/ContactSelectionController.swift`
- `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift`
Pre-existing WIP (`build-system/bazel-rules/sourcekit-bazel-bsp`, `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/` / `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md`) should NOT be staged.
- [ ] **Step 12.2: Stage only the wave-36 files**
Run:
```bash
git add submodules/AccountContext/Sources/ContactSelectionController.swift \
submodules/ContactListUI/Sources/ContactListNode.swift \
submodules/ContactListUI/Sources/ContactsController.swift \
submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift \
submodules/TelegramUI/Sources/ContactMultiselectionController.swift \
submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift \
submodules/TelegramUI/Sources/ContactSelectionController.swift \
submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift
```
If Task 10 introduced additional files (inventory-miss fixes), append them.
- [ ] **Step 12.3: Commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 36: ContactListPeer.peer Peer -> EnginePeer
Migrates the public enum case `ContactListPeer.peer(peer: Peer, isGlobal:,
participantCount:)` from the Postbox `Peer` protocol to the TelegramCore
`EnginePeer` enum. Also cascades `ContactListPeer.indexName` return type
from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` and rewrites
the enum's custom `==` operator to use EnginePeer's synthesized Equatable.
Consumer-side cascade in 7 files:
- 20 `._asPeer()` outflow bridge-drops at ContactListPeer.peer
construction sites (the payload is now EnginePeer)
- 16 `EnginePeer(peer)` inflow wrap-drops at destructure sites (the
destructured `peer` is already EnginePeer)
- 2 `EnginePeer.IndexName(...)` wrap-drops at a sort-comparator (the
indexName property now returns EnginePeer.IndexName directly)
- 2 Postbox-concrete cast chains rewritten to EnginePeer case patterns
(`peer as? TelegramUser``case .user = peer`, etc.)
- `lhsPeer.isEqual(rhsPeer)``lhsPeer == rhsPeer` in the ==operator
Files modified:
submodules/AccountContext/Sources/ContactSelectionController.swift
submodules/ContactListUI/Sources/ContactListNode.swift
submodules/ContactListUI/Sources/ContactsController.swift
submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift
submodules/TelegramUI/Sources/ContactMultiselectionController.swift
submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift
submodules/TelegramUI/Sources/ContactSelectionController.swift
submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift
Bridges intentionally retained (out-of-wave scope):
- `canSendMessagesToPeer(peer._asPeer())` — callee takes Peer, deferred
- `peerTokenTitle(peer: peer._asPeer(), ...)` — callee takes Peer,
deferred
Plan: docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md
Spec: docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 12.4: Update CLAUDE.md wave counter**
Edit `CLAUDE.md` to bump the "Waves landed so far" line from "35 waves" to "36 waves" and update the "as of" date if the commit lands after 2026-04-24.
- [ ] **Step 12.5: Append wave outcome to the postbox-refactor-log**
Append a "Wave 36 outcome" section to `docs/superpowers/postbox-refactor-log.md` documenting:
- Actual files touched + edit counts vs. plan
- Any inventory undercounts surfaced by Task 10 (file:line + missed-category)
- Any lessons learned (e.g., whether the γ category really had zero sites; how the φ cast-rewrites behaved; post-migration undercount percentage vs wave 35's 14%)
- Ratio of bridge-drops to bridge-additions (wave theme: removal-dominated)
Keep concise.
- [ ] **Step 12.6: Commit the docs update**
Run:
```bash
git add CLAUDE.md docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: add wave 36 outcome (ContactListPeer.peer Peer→EnginePeer)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 12.7: Update the next-wave memory**
Edit `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`:
- Add wave 36 to the "Latest commits" section.
- Move ContactListPeer migration from "Recommended wave 36 candidates" to landed.
- Record the inventory undercount ratio (actual-files-touched ÷ pre-flight-file-count) for calibration.
- Update the "Recommended wave 37" section. Promote candidates: `canSendMessagesToPeer(_:)` parameter (the ContactsSearchContainerNode `._asPeer()` bridges at L488/528/562 plus others elsewhere drop when this lands); `peerTokenTitle(peer:)` parameter (drops 3 bridges in ContactMultiselectionController); `makePeerInfoController` / `makeChatQrCodeScreen` / `makeChatRecentActionsController` AccountContext protocol methods (largest remaining Peer-typed-API); accountManager engine path; Shape-C `resourceData` module.
Use the Edit tool on the memory file. No git commit needed.
---
## Risks and notes
- **Inventory undercount.** Pre-flight caught several sites the Explore agent missed (inflow wraps at L481/491/517/527/492/844, cast rewrites at L182-186 and L1968). Budget for 13 additional misses surfacing in Task 10. If the build surfaces 5+ misses in new categories, stop and reassess.
- **`replace_all` usage.** Every `replace_all=true` Edit in this plan is gated by a pre-flight `grep -c` count check. If the count is wrong, fall back to per-site Edits with surrounding context.
- **Cast rewrite at L182-186.** The original cast chain binds `group` and `channel` (but not `user`). The EnginePeer case-pattern form preserves this: `case .user = peer` is a binding-free match, mirroring `if let _ = peer as? TelegramUser`.
- **`._asPeer()` sites that stay.** Tasks 4.3 and 5.3 explicitly verify that the 3 `canSendMessagesToPeer(peer._asPeer())` bridges and 3 `peerTokenTitle(peer: peer._asPeer(), ...)` bridges remain intact. Dropping these would be out-of-scope migration.
- **WIP isolation.** Pre-existing `ChatListFilterPresetController.swift` / `ChatListFilterPresetListController.swift` edits and untracked `build-system/tulsi/`, `submodules/TgVoip/`, `third-party/libx264/` paths are user WIP — do NOT stage them. Use the explicit `git add <files>` form in Step 12.2.
- **Scope boundary.** Task 10 errors surfacing in `TelegramCore`, `Postbox`, or `TelegramApi` mean the migration cascaded beyond its intended consumer scope. Halt and investigate — do NOT edit TelegramCore in this wave.
- **No new typealiases/wrappers.** Rule 2 and 3 of the Postbox refactor guidance apply — this wave stays inside.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,666 @@
# Wave 35: `SendAsPeer.peer: Peer → EnginePeer` Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate the public field `SendAsPeer.peer` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum in a single atomic commit. Drops 3 `._asPeer()` bridges at construction sites, collapses 6 redundant `EnginePeer(peer.peer)` wraps, rewrites 1 `peer.peer as? TelegramChannel` downcast to an enum pattern, and adds `EnginePeer(channel)` wraps at 2 raw-`TelegramChannel` construction sites. No outflow `._asPeer()` bridges need to be added for this wave (unlike wave 34's `ContactListPeer.peer(peer:)` bridge).
**Architecture:** One atomic commit. The field-type change is necessarily atomic (half-migrated SendAsPeer doesn't compile), so all edits land together. TelegramCore's `_internal_*SendAsAvailablePeers` functions keep `import Postbox` — only `SendAsPeer`'s public surface changes. No new wrappers, no new typealiases. The manual `==` body is replaced with synthesized Equatable (EnginePeer is Equatable).
**Tech Stack:** Swift, Bazel build via Make.py wrapper. No tests — verification is build success + targeted grep checks.
**Spec:** `docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md`
---
## File Structure
**Modified files (7 expected — 1 TelegramCore + 6 consumer. Plus 2 "verify no-edit" files.)**
| File | Edit count | Category |
|---|---|---|
| `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift` | ~7 spot edits (struct change + 4 constructor wraps + drop manual `==`) | α |
| `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` | ~5 (1 cast rewrite + 4 wrap drops) | γ |
| `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` | 3 (1 bridge-drop + 2 EnginePeer wraps on raw channel) | δ |
| `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift` | 1 (bridge-drop) | δ |
| `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift` | 1 (wrap collapse) | δ |
| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` | ~4 (1 bridge-drop + 1 flatMap simplify + 1 map simplify) | δ |
**Verify-only (no edits expected):**
| File | Reason |
|---|---|
| `submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift` | Holds `[SendAsPeer]?` at collection level, no `.peer` access. |
| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift` | Passes `currentSendAsPeer` through to `ChatSendAsPeerListContextItem` which keeps taking `[SendAsPeer]`. |
**EnginePeer enum case mapping (used in cast rewrite):**
| Postbox concrete | EnginePeer case |
|---|---|
| `TelegramChannel` | `.channel(TelegramChannel)` |
| `TelegramGroup` | `.legacyGroup(TelegramGroup)` |
| `TelegramUser` | `.user(TelegramUser)` |
---
## Task 1: Edit `SendAsPeers.swift` — struct definition + constructor wraps
**Files:**
- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift`
Foundational change. Without it, none of the consumer edits compile.
- [ ] **Step 1.1: Update the SendAsPeer struct field, init parameter, and drop manual `==`**
Edit:
```swift
// OLD
public struct SendAsPeer: Equatable {
public let peer: Peer
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) {
self.peer = peer
self.subscribers = subscribers
self.isPremiumRequired = isPremiumRequired
}
public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired
}
}
```
```swift
// NEW
public struct SendAsPeer: Equatable {
public let peer: EnginePeer
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: EnginePeer, subscribers: Int32?, isPremiumRequired: Bool) {
self.peer = peer
self.subscribers = subscribers
self.isPremiumRequired = isPremiumRequired
}
}
```
Use the Edit tool with the OLD block as `old_string` and the NEW block as `new_string`. Swift synthesizes Equatable for structs where every stored property is Equatable: `EnginePeer` is Equatable, `Int32?` is Equatable, `Bool` is Equatable — so the manual `==` is no longer needed.
- [ ] **Step 1.2: Wrap raw Postbox `Peer` values at the four constructor sites**
Sites at lines 64, 170, 236, 330. Each binds a raw Postbox `Peer` (from `transaction.getPeer(peerId)` or `peers.map { ... }`) and passes it to the `SendAsPeer(peer: ...)` init. Wrap each with `EnginePeer(...)`.
Edit (line 64, inside `_internal_cachedPeerSendAsAvailablePeers`, cache-hit branch):
```swift
// OLD
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
```
```swift
// NEW
peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
```
Edit (line 170, inside `_internal_peerSendAsAvailablePeers`, network-response map):
```swift
// OLD
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
```
```swift
// NEW
return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
```
Edit (line 236, inside `_internal_cachedLiveStorySendAsAvailablePeers`, cache-hit branch):
```swift
// OLD
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
```
```swift
// NEW
peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
```
Note: lines 64 and 236 have identical text. If you prefer `replace_all=true`, do a grep first to confirm the count is exactly 2, then apply once.
Edit (line 330, inside `_internal_liveStorySendAsAvailablePeers`, network-response map):
```swift
// OLD
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
```
```swift
// NEW
return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
```
Same remark as above: lines 170 and 330 are identical — one `replace_all=true` covers both if the count is exactly 2.
- [ ] **Step 1.3: Verify** — read the updated file and confirm:
- The struct's `peer` field is now `EnginePeer`
- The init parameter is `peer: EnginePeer`
- Manual `==` has been removed
- All 4 constructor sites wrap with `EnginePeer(...)`
- `peer.peer.id` accesses inside the caching loops (lines 87, 90, 259, 262) remain unchanged (`EnginePeer.id` typealias to `PeerId` keeps them valid)
Do not commit yet.
---
## Task 2: Edit `ChatSendAsPeerListContextItem.swift` — cast rewrite + wrap collapse
**Files:**
- Modify: `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift`
1 Postbox-concrete downcast rewrite + 4 `EnginePeer(peer.peer)` wrap drops.
- [ ] **Step 2.1: Rewrite the `peer.peer as? TelegramChannel` downcast at line 73**
Edit:
```swift
// OLD
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel {
if case .broadcast = peer.info {
```
```swift
// NEW
} else if let subscribers = peer.subscribers {
if case let .channel(channel) = peer.peer {
if case .broadcast = channel.info {
```
Note: the original `if let peer = peer.peer as? TelegramChannel` shadows the outer `peer: SendAsPeer` loop variable. The rewrite uses `channel` to avoid shadowing. Any subsequent uses of `peer.info`, `peer.flags`, etc. inside the inner `if let peer = ...` block must be renamed to `channel.*`.
Read lines 7090 before editing to see the full extent of the shadowed-`peer` scope, and ensure every reference to `peer.info` (and any sibling field access like `peer.flags`, `peer.username`, etc.) within the inner block is rewritten to `channel.*`. The snippet above captures the only `peer.info` site from the inventory.
- [ ] **Step 2.2: Drop `EnginePeer(peer.peer)` wraps at lines 89, 110, 116, 121**
The field `peer.peer` is now `EnginePeer`, so `EnginePeer(peer.peer)` becomes a type error. Drop the wrap.
Read the full lines first to confirm each site's shape. Expected patterns (edit one at a time with enough surrounding context to make each unique — the four sites likely differ in surrounding tokens):
For each of the four sites, the pattern to eliminate is `EnginePeer(peer.peer)``peer.peer`. Example:
```swift
// OLD
let title = EnginePeer(peer.peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
```
```swift
// NEW
let title = peer.peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
```
Identify each of the four sites (lines 89, 110, 116, 121) by reading the file, then apply one Edit per site using enough surrounding context (usually 12 tokens before/after the `EnginePeer(peer.peer)` subexpression) to make the `old_string` unique.
If all four lines reduce to the same substring pattern (e.g., `EnginePeer(peer.peer)` as a standalone subexpression), `replace_all=true` on the substring `EnginePeer(peer.peer)``peer.peer` is safe — but **first** grep to confirm the count is exactly 4 and no other meaning is captured.
Run before: `grep -cE "EnginePeer\(peer\.peer\)" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift`
Expected: 4.
- [ ] **Step 2.3: Verify** — grep:
Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift`
Expected: zero matches.
---
## Task 3: Edit `ChatControllerLoadDisplayNode.swift` — bridge-drop + raw-channel wraps
**Files:**
- Modify: `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift`
1 `._asPeer()` bridge-drop at line 772 + 2 `EnginePeer(channel)` wraps for raw `TelegramChannel` at lines 805 and 823.
- [ ] **Step 3.1: Bridge-drop at line 772**
Edit:
```swift
// OLD
return SendAsPeer(peer: peer._asPeer(), subscribers: nil, isPremiumRequired: false)
```
```swift
// NEW
return SendAsPeer(peer: peer, subscribers: nil, isPremiumRequired: false)
```
Verification: the surrounding signal chain binds `peer` as `EnginePeer` (from `context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: ...))`). The `._asPeer()` bridge is no longer needed.
If the line text differs from the OLD block above (e.g., different field order or trailing arguments), read the file around line 772 and adjust the `old_string` to match byte-for-byte before editing.
- [ ] **Step 3.2: Wrap raw `TelegramChannel` at line 805**
Read lines 800812 to see the bound `channel` variable. The construction site should be `SendAsPeer(peer: channel, ...)` where `channel: TelegramChannel` is raw Postbox.
Edit:
```swift
// OLD
SendAsPeer(peer: channel, subscribers: subscribers, isPremiumRequired: isPremiumRequired)
```
```swift
// NEW
SendAsPeer(peer: EnginePeer(channel), subscribers: subscribers, isPremiumRequired: isPremiumRequired)
```
If the surrounding context differs (different field values), match the actual line text when writing `old_string`.
- [ ] **Step 3.3: Wrap raw `TelegramChannel` at line 823**
Same pattern as Step 3.2. Read lines 818830 first, identify the `SendAsPeer(peer: channel, ...)` construction site, and wrap `channel` with `EnginePeer(...)`.
If the line text at 805 and 823 is identical, `replace_all=true` on the substring `SendAsPeer(peer: channel,``SendAsPeer(peer: EnginePeer(channel),` covers both. **First** grep to confirm the count:
Run before: `grep -cE "SendAsPeer\(peer: channel," submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift`
Expected: 2.
- [ ] **Step 3.4: Verify** — grep:
Run: `grep -nE "SendAsPeer\(peer:\s+\w+\._asPeer\(\)|SendAsPeer\(peer:\s+channel," submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift`
Expected: zero matches. Lines 792, 826, 835, 844 retaining `.peer.id` accesses are expected and correct.
---
## Task 4: Edit `ChatTextInputPanelComponent.swift` — bridge-drop
**Files:**
- Modify: `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift`
1 `._asPeer()` bridge-drop.
- [ ] **Step 4.1: Bridge-drop at line 847**
Read lines 843853 to confirm the surrounding signal chain and the type of `sendAsConfiguration.currentPeer` (expected: `EnginePeer`).
Edit:
```swift
// OLD
let sendAsPeers = [SendAsPeer(peer: sendAsConfiguration.currentPeer._asPeer(), subscribers: nil, isPremiumRequired: false)]
```
```swift
// NEW
let sendAsPeers = [SendAsPeer(peer: sendAsConfiguration.currentPeer, subscribers: nil, isPremiumRequired: false)]
```
If the actual line text wraps across multiple lines or uses different field values, match the real text byte-for-byte when writing `old_string`.
- [ ] **Step 4.2: Verify** — grep:
Run: `grep -nE "SendAsPeer\(peer:.*\._asPeer\(\)" submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift`
Expected: zero matches.
---
## Task 5: Edit `ChatTextInputPanelNode.swift` — wrap collapse
**Files:**
- Modify: `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift`
1 `EnginePeer(peer)` wrap collapse at line 1625.
- [ ] **Step 5.1: Collapse `EnginePeer(peer)` wrap**
Read lines 16151630 to see the full context. `peer` is bound from a preceding `var currentPeer = sendAsPeers.first(where: { $0.peer.id == ... })?.peer` (lines 16201622). After migration, `.peer` returns `EnginePeer`, so `EnginePeer(peer)` on an `EnginePeer` is a type error.
Exact edit depends on the actual line text. Example shape:
```swift
// OLD (at or near line 1625)
let enginePeer = EnginePeer(peer)
```
```swift
// NEW
let enginePeer = peer
```
Read lines 16231628 first and write the Edit with byte-accurate `old_string`. If the bound variable is then used as `enginePeer.displayTitle(...)`, consider whether the rename can be eliminated entirely (e.g., rename `peer` uses downstream), but prefer the minimal edit for commit clarity.
Lines 1616, 1620, 1622, 2948, 5370 should remain unchanged — they perform `.peer.id` comparisons or `.first(where:)` lookups that work identically on `[SendAsPeer]` with `EnginePeer`-typed `.peer`.
- [ ] **Step 5.2: Verify** — grep:
Run: `grep -nE "EnginePeer\(peer\)" submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift`
Expected: zero matches. If any remain, inspect each — they may be unrelated wraps on non-SendAsPeer-sourced `peer` variables (in which case they must stay).
---
## Task 6: Edit `StoryItemSetContainerViewSendMessage.swift` — multi-site cleanup
**Files:**
- Modify: `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift`
1 bridge-drop + 1 flatMap simplify + 1 map simplify. Many other `.peer.id` / `.peer` accesses remain unchanged.
- [ ] **Step 6.1: Bridge-drop at line 249**
Read lines 244254 to confirm `accountPeer` is typed as `EnginePeer` upstream.
Edit:
```swift
// OLD
availablePeers.append(SendAsPeer(
peer: accountPeer._asPeer(),
subscribers: nil,
isPremiumRequired: false
))
```
```swift
// NEW
availablePeers.append(SendAsPeer(
peer: accountPeer,
subscribers: nil,
isPremiumRequired: false
))
```
If the actual layout (whitespace, line breaks) differs from the OLD block, match the real text byte-for-byte when writing `old_string`.
- [ ] **Step 6.2: Simplify flatMap at line 4080**
`EnginePeer.init` as a function reference expects a raw `Peer` and returns `EnginePeer`. After migration, `sendAsPeer?.peer` is already `EnginePeer?`, so `.flatMap(EnginePeer.init)` is both unnecessary and a type error.
Edit:
```swift
// OLD
myPeer: (sendAsPeer?.peer).flatMap(EnginePeer.init),
```
```swift
// NEW
myPeer: sendAsPeer?.peer,
```
Read lines 40784082 first to confirm the surrounding labeled-argument layout and match byte-for-byte.
- [ ] **Step 6.3: Simplify map at line 4081**
`.map({ EnginePeer($0.peer) })` wraps each already-`EnginePeer` value in `EnginePeer(...)` — a type error. Drop the wrap.
Edit:
```swift
// OLD
availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ EnginePeer($0.peer) }) ?? []),
```
```swift
// NEW
availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ $0.peer }) ?? []),
```
Read lines 40794083 first to confirm the exact line text.
- [ ] **Step 6.4: Verify** — grep:
Run: `grep -nE "SendAsPeer\(peer:.*\._asPeer\(\)|EnginePeer\(\$0\.peer\)|\(sendAsPeer\?\.peer\)\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift`
Expected: zero matches.
Retained-as-is accesses (inventory-verified correct after migration): `.peer.id` at lines 254, 688, 4088, 4089, 4327, 4333, 4340, 4356, 4372; optional chaining at 4050, 4068, 4069. These should NOT be edited.
---
## Task 7: Verify "no-edit" consumer files
**Files:**
- Read: `submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift`
- Read: `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift`
Sanity-check: confirm neither file contains `.peer as?`/`is`, `EnginePeer(.peer)`, or `._asPeer()` patterns tied to SendAsPeer. If any such pattern is found, fold the fix into the relevant task above before the build pass.
- [ ] **Step 7.1: Grep ChatPresentationInterfaceState.swift**
Run: `grep -nE "SendAsPeer|sendAsPeers" submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift`
Expected shape: field declaration, init param, assignment, equality comparison, `updatedSendAsPeers(_:)` method — all at the `[SendAsPeer]?` collection level. No `.peer` field access.
- [ ] **Step 7.2: Grep StoryItemSetContainerComponent.swift**
Run: `grep -nE "SendAsPeer|currentSendAsPeer|\.peer\b" submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift | grep -iE "sendAsPeer|\.peer"`
Read lines 30563072 to confirm `sendMessageContext.currentSendAsPeer` is only passed through to `ChatSendAsPeerListContextItem` (which keeps `[SendAsPeer]`) or accessed for `.peer.id` comparisons — neither requires an edit.
If the verification shows an edit is needed, add the edit as an additional step under the relevant Task 26. Do not edit here silently.
---
## Task 8: Build verification (first pass)
- [ ] **Step 8.1: Run the full build with `--continueOnError`**
Run:
```bash
source ~/.zshrc 2>/dev/null && python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError 2>&1 | tee /tmp/wave35-build.log
```
Expected outcome: ideally clean. Realistic outcome: 05 errors at sites the inventory missed.
- [ ] **Step 8.2: Triage build errors**
Likely error patterns and their fixes:
| Error | Fix |
|---|---|
| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer'` at site passing `peer.peer` | Add `._asPeer()` bridge: `peer.peer._asPeer()` |
| `cannot convert value of type 'Peer' to expected argument type 'EnginePeer'` at `SendAsPeer(peer: ...)` | Add wrap: `SendAsPeer(peer: EnginePeer(<raw>), ...)` |
| `value of type 'EnginePeer' has no member 'isEqual'` | Replace with `==` |
| `pattern of type 'TelegramChannel' cannot match values of type 'EnginePeer'` | Missed C2 — rewrite to `if case .channel(let channel) = peer.peer` form |
| `cannot invoke initializer for type 'EnginePeer' with an argument list of type '(EnginePeer)'` | Missed wrap collapse — drop `EnginePeer(...)` |
| `extraneous argument label 'peer:' in call` or similar on `SendAsPeer(...)` | Check that the construction arg is `EnginePeer`, not raw — add `EnginePeer(...)` wrap |
For each error, identify the file:line, apply the appropriate fix, and re-run the build until clean.
- [ ] **Step 8.3: Iterate to clean build**
Re-run the build after each batch of fixes. The wave is complete when the build returns 0 errors for the targeted configuration.
If 10+ unexpected errors surface, halt and reassess: the inventory was significantly incomplete and the wave may need to be split into pre-cleanup commits. Discuss with user before continuing.
---
## Task 9: Post-build grep validations
- [ ] **Step 9.1: Bridge-drop validation**
Run:
```bash
grep -rn "SendAsPeer(peer:.*\._asPeer()" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/" | grep -v "^submodules/Postbox/"
```
Expected: zero hits. If any remain, those are missed bridge-drops — fix and re-run Task 8.
- [ ] **Step 9.2: Wrap-collapse validation**
Run:
```bash
for f in submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift \
submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift \
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift \
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift \
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift; do
echo "=== $f ==="
grep -nE "EnginePeer\(peer\.peer\)|EnginePeer\(\$0\.peer\)|\(sendAsPeer\?\.peer\)\.flatMap\(EnginePeer\.init\)" "$f"
done
```
Expected: zero hits across all 5 files.
- [ ] **Step 9.3: C2 cast validation**
Run:
```bash
grep -nE "peer\.peer\s+(as\?|is)\s+Telegram" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift
```
Expected: zero hits.
- [ ] **Step 9.4: Construction-site validation**
Ensure all `SendAsPeer(peer: ...)` construction sites outside TelegramCore provide `EnginePeer`:
```bash
grep -rnE "SendAsPeer\(peer:" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/"
```
Inspect each hit. Expected forms: `SendAsPeer(peer: <engine-peer-expr>, ...)` or `SendAsPeer(peer: EnginePeer(<raw>), ...)`. Anything of the form `SendAsPeer(peer: <raw-Peer>, ...)` is a miss — fix.
If any of the validations fail, return to Task 8 to fix.
---
## Task 10: Atomic commit + memory + log update
- [ ] **Step 10.1: Stage and review**
Run:
```bash
git status --short
git diff --stat
```
Confirm exactly 6 modified Swift files (1 TelegramCore + 5 consumer — or 7 if Task 7 surfaced a needed edit). Files expected:
- `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift`
- `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift`
- `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift`
- `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift`
- `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift`
- `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift`
WIP from earlier (`build-system/bazel-rules/sourcekit-bazel-bsp`, `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/`) should NOT be staged.
The `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md` untracked file should ALSO remain unstaged.
- [ ] **Step 10.2: Stage only the wave-35 files**
Run:
```bash
git add submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift \
submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift \
submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift \
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift \
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift \
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
```
If Task 7 surfaced an additional file, append it here.
- [ ] **Step 10.3: Commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 35: SendAsPeer.peer Peer -> EnginePeer
Migrates the public field `SendAsPeer.peer` from the Postbox `Peer`
protocol to the TelegramCore `EnginePeer` enum. Internal
`_internal_*SendAsAvailablePeers` bodies keep `import Postbox` (they still
call `postbox.transaction`) and wrap raw peer values with `EnginePeer(peer)`
at the SendAsPeer constructor sites. Manual `==` body dropped in favor of
synthesized Equatable.
Consumer-side cascade in 5 files:
- 3 `._asPeer()` bridge-drops at SendAsPeer constructor sites
- 6 redundant `EnginePeer(peer.peer)` / `EnginePeer($0.peer)` wrap
drops (the field is now EnginePeer, so the wrap fails to compile)
- 1 `peer.peer as? TelegramChannel` downcast rewritten to
`if case let .channel(channel) = peer.peer` enum-pattern form
- 2 `EnginePeer(channel)` wraps added where raw `TelegramChannel` is
passed into `SendAsPeer(peer: ...)`
- 1 `(sendAsPeer?.peer).flatMap(EnginePeer.init)` simplified to
`sendAsPeer?.peer` (already `EnginePeer?`)
Files modified:
submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift
submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift
submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift
submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
Plan: docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md
Spec: docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 10.4: Update CLAUDE.md wave counter**
Edit `CLAUDE.md` to bump the "Waves landed so far" line from "34 waves" to "35 waves" and update the "as of" date if the commit lands after 2026-04-24.
- [ ] **Step 10.5: Append wave outcome to the postbox-refactor-log**
Append a "Wave 35 outcome" section to `docs/superpowers/postbox-refactor-log.md` documenting:
- Actual files touched and edit counts vs. plan
- Any inventory undercounts surfaced by Task 8
- Any lessons learned (e.g., whether the flatMap/map simplifications were actually type-required or whether they could have been left as redundant-but-compiling wraps)
Keep concise.
- [ ] **Step 10.6: Commit the docs update**
Run:
```bash
git add CLAUDE.md docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: add wave 35 outcome (SendAsPeer.peer Peer→EnginePeer)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 10.7: Update the next-wave memory**
Update `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`:
- Add wave 35 to the "Latest commits" section
- Move SendAsPeer migration from "Wave 34+ candidates → Downstream Peer-typed APIs" to landed
- Record the inventory undercount ratio (actual-files-touched ÷ pre-flight-file-count) for calibration of future Peer-typed-API waves
- Update the "Recommended wave 35" section to reflect the new wave 36 recommendation. Candidates to promote: `makePeerInfoController` (largest Peer-typed-API remaining), `ContactListPeer.peer(peer:)` case payload, `canSendMessagesToPeer(_:)` parameter, accountManager-side engine path, Shape-C resourceData module pick
Use the Edit tool on the memory file. No git commit needed (memory lives outside the repo).
---
## Risks and notes
- **Inner `peer` shadowing in ChatSendAsPeerListContextItem:73.** The original `if let peer = peer.peer as? TelegramChannel` shadows the outer `peer: SendAsPeer` loop variable. The rewrite uses `channel` to avoid shadowing. Verify every reference to `peer.info` (and any sibling field access) within the old inner-if scope is updated to `channel.*` — Step 2.1's instructions cover this, but it's easy to miss a field reference.
- **`replace_all` correctness.** Whenever the plan suggests `replace_all=true`, verify the count first via grep. If the count is unexpected, revert to per-site Edits with surrounding context.
- **Inventory undercount.** Wave 34 undercounted by ~30%. The Explore agent for wave 35 explicitly included `.peer as?`/`is`/outflow-helper patterns, so the expected ratio is lower, but budget for 13 inventory-missed sites surfacing in Task 8.
- **Name collisions (do NOT touch).** `[EnginePeer]` arrays in `LiveStreamSettingsScreen.swift`, `ShareWithPeersScreen.swift`, and `ChatSendStarsScreen.swift` named `sendAsPeers` / `availableSendAsPeers` are unrelated. `ChatPanelInterfaceInteraction` callbacks named `openSendAsPeer` take `(ASDisplayNode, ContextGesture?)`, not `SendAsPeer`. `initialSendAsPeerId` parameters are `PeerId`-typed. If Task 8 surfaces errors in any of these files, the fix likely indicates a wrong cascade from a real SendAsPeer site — do NOT migrate those files as part of this wave.
- **WIP isolation.** Pre-existing modifications to `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, the `sourcekit-bazel-bsp` submodule marker, and untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/` / `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md` are user WIP — do NOT stage them. Use the explicit `git add <files>` form in Step 10.2.

View file

@ -632,6 +632,32 @@ Net: 3 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources
Plan / record: (no plan doc this wave — mechanical sweep).
## Wave 27 outcome (2026-04-22)
`preferencesView` consumer sweep (wave-9 pattern continuation). No new TelegramCore facades — leverages existing `TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key:)`.
**Shape.** Replace `context.account.postbox.preferencesView(keys: [<key>])` — returning `Signal<PreferencesView, NoError>` — with `context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: <key>))` — returning `Signal<PreferencesEntry?, NoError>`. Downstream, rename `<name>.values[<key>]?.get(<Type>.self)``<name>?.get(<Type>.self)` at each closure parameter.
**30 consumer files, ~40 call sites migrated** across ChatListUI, ContactListUI, DebugSettingsUI, GalleryUI, PeersNearbyUI, SettingsUI, TelegramCallsUI, TelegramUI, TelegramUI/Components, WebSearchUI. Full list in `git show --stat <wave-27-commit>`.
**Multi-key sites (PresentationCallManager).** 3 sites used `preferencesView(keys: [voipConfiguration, appConfiguration])`. Migrated via the two-arg `engine.data.subscribe(itemA, itemB) |> take(1)` overload, which returns `Signal<(PreferencesEntry?, PreferencesEntry?), NoError>`. Closures that accessed `preferences.values[X]?.get(...)` rewritten to `preferences.0?.get(...)` and `preferences.1?.get(...)`.
**Direct-postbox-param helper migrated.** `AccountContext.swift`'s `getAppConfiguration(postbox: Postbox)` helper (one internal caller only) was rewritten to `getAppConfiguration(engine: TelegramEngine)` in the same commit, switching its single call site from `getAppConfiguration(postbox: account.postbox)` to `getAppConfiguration(engine: self.engine)`.
**Annotation update in NotificationExceptionControllerNode.swift.** An explicit signal type `Signal<(…, PreferencesView, …), NoError>` in a `mapToSignal` return was updated to `Signal<(…, PreferencesEntry?, …), NoError>`. The file still imports Postbox because `PreferencesEntry` is (for now) a Postbox-defined type surfaced through TelegramCore's `EnginePreferencesEntry` typealias — a future wave-6-style `import Postbox` sweep would clean up such imports where they're now the only Postbox reference.
**Deliberately skipped in this wave.**
- `TelegramPermissionsUI/Sources/PermissionSplitTest.swift:100``permissionUISplitTest(postbox: Postbox)` is a public API whose product value `PermissionUISplitTest` itself stores `postbox: Postbox` to satisfy the `SplitTest` protocol. Proper migration requires a protocol-level refactor (or wholesale rewrite of the SplitTest abstraction) beyond this wave's scope.
- 5 TelegramCore-internal `postbox.preferencesView(...)` sites (ChatListFiltering × 3, ContentSettings × 1, ManagedGlobalNotificationSettings × 1) — the refactor only migrates consumer modules, not TelegramCore internals.
**Build validation.** Clean first-pass build (748 processes, 227s, 0 errors). No new facades to test, shape was validated across 30 files on the first attempt.
**Lesson — multi-key preferencesView migration.** `engine.data.subscribe(itemA, itemB)` exists and returns a Swift tuple. When a Postbox `preferencesView(keys: [K1, K2])` call is inside a `combineLatest(...)` whose downstream closure accesses `.values[K1]` and `.values[K2]`, prefer the two-arg subscribe form (vs. two separate subscribes combined externally) — it preserves `combineLatest` arity exactly. Rewrite `.values[K1]?.get(T.self)``.0?.get(T.self)`, `.values[K2]?.get(T.self)``.1?.get(T.self)`. The closure parameter name stays (e.g., `preferences`) because the tuple destructure preserves the variable-name semantics at the call site.
Net: 30 consumer files. No TelegramCore changes. CLAUDE.md facade-inventory table unchanged (no new facades).
Plan / record: `memory/project_postbox_wave27_plan.md` (deleted post-wave).
## Wave 26 outcome (2026-04-21)
`resourceRangesStatus` + `removeCachedResources` facade additions + consumer sweep. Combines two independent small sweeps into one commit.
@ -657,6 +683,267 @@ Net: 3 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources
Plan / record: (no plan doc this wave — mechanical sweep).
## Wave 31 outcome (2026-04-23)
Second build-verified `^import Postbox$` sweep on consumer modules since wave 6 (2026-04-19). Same methodology: speculative-drop + `--continueOnError` build loop with pattern-based preemptive restores.
**Candidate set narrowing.** Initial candidate grep `grep -rl "^import Postbox$" submodules --include="*.swift"` returned **1184** files. 606 of those live in `submodules/TelegramCore/Sources/` — TelegramCore legitimately `import Postbox`; the TelegramCore files were accidentally included and had to be reverted via `git checkout -- submodules/TelegramCore/Sources/` before re-seeding the drop. Final consumer candidate set: **578** files. **Lesson for future sweep invocations: the candidate-set grep must filter out `submodules/TelegramCore/` as well as `submodules/Postbox/` / `submodules/TelegramApi/`.** Wave 6's methodology note at step 1 (line 37) already calls this out, but the TelegramCore carve-out is easy to miss because TelegramCore doesn't `@_exported import Postbox`, so from a pure re-exports perspective it's indistinguishable from a consumer.
**9 build iterations to convergence** (plus 1 aborted first iteration for the TelegramCore scope error). Per-iteration failure counts: 18 → 2 → 9 → 12 → 1 → 1 → 3 → 1 → 4 → 0. Surfacing pattern was typical of a speculative-drop sweep: errors bubble one dependency-graph layer at a time.
**Per-iteration symbol expansion.** The wave-6 preemptive-restore symbol list (CLAUDE.md's "Unused-import sweeps" guidance) needed extensions for this sweep:
- Iter 3 surfaced `CodableEntry`, `CachedMediaResourceRepresentation`, `CachedMediaRepresentationKeepDuration`.
- Iter 4 surfaced `PostboxViewKey`, `OrderedItemListView`, `UnreadMessageCountsItem`, `ChatListEntrySummaryComponents`, `PeerStoryStats`, `ItemCollectionId` (note: typealias `EngineItemCollectionId` exists but raw name still requires `import Postbox`), and broadened `\bMedia\b`, `\bMessage\b`, `\bPeer\b`.
- Iter 5 surfaced `FetchResourceSourceType` (same typealias caveat).
- Iter 6 surfaced `StoryId`.
- Iter 7 surfaced `ChatListIndex`.
- Iter 8 surfaced `PreferencesEntry` (typealias caveat), `PeerView`, `RenderedPeer`.
- Iter 9 surfaced `declareEncodable`.
- Iter 10 surfaced `ItemCollectionItemIndex`, `ValueBoxEncryptionParameters`, `fileSize`, plus a restore-script bug (see below).
**Restore-script bug: `#if canImport(...)` blocks.** The naive restore inserter picks the last `^import ` line and appends `import Postbox` after it. If the last import sits inside an `#if canImport(AppCenter) ... #endif` preprocessor block, the restored `import Postbox` lands inside that block and is only active under that configuration. `AppDelegate.swift` in `submodules/TelegramUI/Sources/` hit this (original had `import Postbox` at line 7; drop + restore put it inside the `#if canImport(AppCenter)` block at line 51); the build failed in iter10 on `cannot find type 'Postbox' in scope` errors even though a literal `grep ^import Postbox$` matched. Fixed by manually moving the import out of the `#if` block. **Lesson for future restore-script work: insert the restored `import Postbox` BEFORE the first `#if` or `#endif` line, not after the last `import` line, to avoid preprocessor-scope traps.**
**Results: 9 source-level surviving drops + 2 duplicate-import dedups.** Final diff: 11 files changed, +2 / -13.
Surviving drops:
- `submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift`
- `submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift`
- `submodules/DebugSettingsUI/Sources/DebugAccountsController.swift`
- `submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift`
- `submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift`
- `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift`
- `submodules/TelegramUI/Sources/ChatLinkPreview.swift`
- `submodules/TelegramUI/Sources/ChatSearchResultsController.swift`
- `submodules/TelegramUI/Sources/MediaManager.swift`
Duplicate-import dedups (files had two `^import Postbox$` lines; kept exactly one — unrelated-but-latent cleanup surfaced incidentally by the sweep):
- `submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift` (2 imports → 1)
- `submodules/TelegramUI/Sources/ChatHistoryListNode.swift` (2 imports → 1)
**Spurious-diff cleanup step (new procedure, adopted this wave).** After convergence, `git diff --numstat` showed 564 modified files but only 9 were genuine drops. The other 553 were "1 addition + 1 deletion" — files where the original `import Postbox` at line X was deleted by the drop and re-inserted at line Y by the restore (different position because restore inserts after "last import line" regardless of original placement). These aren't semantic changes but do produce noisy diffs. Identified via `git diff --numstat | awk '$1 == 1 && $2 == 1 {print $3}'` and reverted via `xargs -I{} git checkout -- {}`. **Lesson: the wave-6 methodology should add a post-convergence "revert 1-add-1-del spurious diffs" step before committing. Alternative: improve the restore script to insert at the exact original line. Either way, the final diff should be limited to real semantic changes.**
**No modules became fully Postbox-free this wave.** Each of the five containing modules still has other files importing Postbox (TelegramUI: 350 remaining, LegacyDataImport: 4, MediaPlayer: 9, AuthorizationUI: 2, DebugSettingsUI: 1). By this point most trivially-droppable imports have been drained; the remaining Postbox-importing files mostly carry real usage. **Re-run cadence lesson: yield per re-run is declining.** Wave 6 yielded 183 drops + 189 modules freed; wave 31 yielded 9 drops + 0 modules freed. Consider spacing future sweeps to every 46 facade waves rather than 23.
**Wave 14 BUILD-dep sweep companion: 0 drops.** Ran the wave-14-style `find submodules -name BUILD | filter-by-no-source-import` check: **0 BUILD candidates**. The 191 BUILDs still listing `//submodules/Postbox` all have at least one Sources/*.swift that actually imports Postbox. One outlier (`submodules/SpotlightSupport/BUILD`) has zero source files but a non-trivial `deps = [...]` list including `//submodules/Postbox`; deliberately left alone (stale-BUILD-on-empty-module is a different class of cleanup and carries unknown side effects).
Net: 11 files changed (9 + 2), +2 / -13 lines. Clean first-attempt verification build without `--continueOnError` (880 actions, 1354 action cache hits, 262s).
Plan / record: (no plan doc this wave — mechanical sweep).
## Wave 32 outcome (2026-04-24)
`resourceStatus` residue sweep. One new facade overload (`status(id:resourceSize:)`) + 4 migrated sites across 2 consumer files. Commit `289fc908bc`.
**Facade added** in `TelegramEngineResources.swift`:
- `status(id: EngineMediaResource.Id, resourceSize: Int64) -> Signal<EngineMediaResource.FetchStatus, NoError>` wraps Postbox's `resourceStatus(MediaResourceId, resourceSize:)` overload. Body mirrors the existing `status(resource:)` facade, converting id via `MediaResourceId(id.stringRepresentation)` and mapping the result via `EngineMediaResource.FetchStatus.init`.
**4 migrated sites (2 files):**
- `ChatListSearchContainerNode.swift:1059` — new `status(id:resourceSize:)` overload. Caller supplies `EngineMediaResource.Id(downloadResource.id)` directly (String initializer; `downloadResource.id: String`) — no raw `MediaResourceId(...)` wrap needed. Mirrors the pre-existing `EngineMediaResource.Id(downloadResource.id)` usage at line 1107.
- `ChatMessageInteractiveMediaNode.swift:1769` — existing `status(resource:)` facade (wave 3).
- `ChatMessageInteractiveMediaNode.swift:1799` — same.
- `ChatMessageInteractiveMediaNode.swift:1809` — existing `resourceRangesStatus(resource:)` facade (wave 26).
**Local preserved deliberately.** `let postbox = context.account.postbox` at `ChatMessageInteractiveMediaNode.swift:1767` stays because line 1793 feeds `postbox` to `HLSVideoContent.minimizedHLSQualityPreloadData(postbox: Postbox, ...)` — that is a third-party-function boundary needing raw `Postbox`. Only the `resourceStatus`/`resourceRangesStatus` call sites within that scope migrate.
**Case-pattern sharing.** `MediaResourceStatus` (raw Postbox) and `EngineMediaResource.FetchStatus` (engine wrapper) have identical case names (`.Fetching`, `.Paused`, `.Local`, `.Remote`). The inner `switch status` at 1770-1779 keeps its `MediaResourceStatus` return type annotation — input case matching works for the engine type, constructed `MediaResourceStatus` return values still compile (`MediaResourceStatus` is in scope via `import Postbox` on line 4). This is the wave-29/30 lesson in action: no enum-case edits required.
**Inventory scope narrowing from memory's prediction.** The memory's `wave 32+ candidates` section predicted ~12 Shape-B/C sites in the residue sweep. Execution-time re-grep reclassified most of them:
- **Coupled to `accountManager.mediaBox.resourceStatus` siblings (6 sites in 3 files):** `ThemePreviewControllerNode:271+277`, `WallpaperGalleryItem:799+805+834+840`, `SettingsThemeWallpaperNode:284+285`. Each pair has an `accountManager`-sourced fallback whose return type is raw `Signal<MediaResourceStatus, NoError>`. Migrating only the `account.postbox` branch breaks the shared sibling type at the `mapToSignal`/`combineLatest` merge point. Deferred until accountManager-side has an engine facade.
- **Shape-C init-param refactor (3 sites in 3 files):** `LegacyWebSearchGallery:248` (free function `legacyWebSearchItem(account: Account, ...)`), `NativeVideoContent:455` (init takes `postbox: Postbox`), `VerticalListContextResultsChatInputPanelItem:229` (item stores `account: Account`). Each needs an init-param change + caller threading — per-module mini-refactor, not wave-shape-G territory.
- **`approximateSynchronousValue` overload:** only call site (`SettingsThemeWallpaperNode:284`) is in the accountManager-coupled bucket above. Adding the facade now would land dead code.
Effective wave scope: 4 sites (the uncoupled subset). Still worth committing as its own wave — closes the `resourceStatus` arc for every site where migration is currently unblocked.
**Build validation.** Clean build (558 processes, 236s, 0 errors). No `--continueOnError` needed — first attempt green.
**Lesson — siblings-define-scope in resource-status migrations.** When an assignment uses `A.resourceStatus(...)` in one branch and `B.resourceStatus(...)` in another (via `if`/`mapToSignal`/`combineLatest`), the branches' return types must match. If `A` has an engine facade but `B` does not (e.g., `accountManager.mediaBox` has no engine wrapper yet), neither branch is migratable in isolation — the whole group must wait. Pre-flight sibling-check for each `resourceStatus` hit: is the enclosing `statusSignal = ...` expression a single source or a multi-source merge?
**Lesson — Shape-B/C classification requires read, not grep.** The memory's wave-32 candidate table classified sites by single-line grep ("`account.postbox.mediaBox.resourceStatus`"). That pattern matches both the fully-migratable `context.account.postbox.mediaBox.X` form (Shape-A via AccountContext) AND the `(local) account.postbox.mediaBox.X` Shape-C form (requires init-param refactor). Distinguishing requires reading 5-10 lines of context to find the `account` binding: field? local? init param? closure capture? Add this as a mandatory step in the per-site inventory for future residue waves.
Plan / record: (no plan doc this wave — small residue sweep).
---
## Wave 33 outcome (2026-04-24)
`loadedPeerWithId` consumer sweep. 60 sites migrated across 37 consumer files. No new facades, no typealiases. Commit `16d017853a`.
**Migration pattern** (per user's explicit direction):
```swift
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
```
This replaces `context.account.postbox.loadedPeerWithId(peerId)` while preserving signature shape. The `mapToSignal` wrapper is critical: Postbox's `loadedPeerWithId` returns `.never()` (signal never emits) when the peer is missing — it does NOT wait for loading. The engine-data equivalent `get(Peer.Peer(id:))` returns `Signal<EnginePeer?, NoError>` (optional snapshot). Unwrapping with `.never()`-on-nil preserves original semantics exactly, while keeping the outer shape `Signal<EnginePeer, NoError>` non-optional so callers' closures don't have to cascade new optional handling.
**Category distribution (per pre-flight Explore catalog, 60 sites):**
| Category | Count | Body change |
|---|---|---|
| Cat-A (trivial) | 22 | Only EnginePeer-compatible members; type swap only. |
| Cat-B (concrete-type cast) | 25 | `peer as? TelegramUser/Group/Channel/SecretChat``if case let .user(user)` (etc.). |
| Cat-C (feeds Peer-typed API) | 13 | `peer._asPeer()` at call point (`makePeerInfoController`, `makeChatRecentActionsController`, `makeChatQrCodeScreen`, `FoundPeer.init`, `SendAsPeer.init`). |
(Cat-B + Cat-C bumped slightly from Explore's catalog after in-edit reclassifications.)
**Engine-access variations:**
- Most consumer modules use `context.engine.data.get(...)` on `AccountContext`.
- `ShareSearchContainerNode.swift` uses `context.engineData.get(...)` because `ShareControllerAccountContext` exposes `engineData: TelegramEngine.EngineData` but not a full `engine`.
- `CallStatusBarNode.swift` (has raw `account: Account` from switch case) constructs `TelegramEngine(account: account)` inline.
- `PresentationGroupCall.swift` uses `self.accountContext.engine.data` instead of the stored `self.account.postbox`.
**TelegramCore internal sites (36) unchanged.** `Postbox.swift` (2 defs), `State/AccountViewTracker.swift`, `State/FetchChatList.swift`, `State/SynchronizePeerReadState.swift`, `Suggestions.swift`, and all `TelegramCore/Sources/TelegramEngine/` internal `_internal_*` helpers still call `postbox.loadedPeerWithId(...)` — they are the Postbox-facing layer.
**Pre-flight efficiency.** An Explore subagent cataloged all 60 sites by category from a single prompt (one-line-per-site output). That catalog made the sweep straightforward: most files fell into identical patterns, enabling template-substitution Edits. Total context spent on discovery was small compared to doing 60 per-site full reads in the main thread.
**Build validation.** First-pass clean build (47 actions, 70s) after sweep completion. Earlier pilot (2 sites, 20s) validated pattern before scaling to all 60.
**Lessons:**
- **`loadedPeerWithId` returns `.never()` on missing peer, not a pending Signal.** Old common misreading: treating it as a "wait-until-loaded" primitive. Actual Postbox source at `Postbox.swift:3925`: `if let peer = self.peerTable.get(id) { return .single(peer) } else { return .never() }`. Preserve this by wrapping `engine.data.get` in `mapToSignal` with the `.never()` fallback — don't replace with plain `|> compactMap { $0 }` (which would drop the signal entirely rather than completing immediately when peer exists).
- **"Keep the signatures to help the typechecker" as a migration principle.** The user (2026-04-24) explicitly directed: keep call-site outer Signal signatures stable (`Signal<EnginePeer, NoError>` non-optional), even at the cost of a 6-line inline `mapToSignal` wrapper at each site. Rationale: 60 sites × optional-cascade body changes > 60 × 6-line wrapper. This is a general principle for sweeps — if the alternative is rewriting every body to handle optionals, prefer the signal-level wrapper to contain the change.
- **Pre-flight cataloging via Explore subagent.** For sweeps with variable per-site body shapes (unlike facade-migration-with-identical-call-expression sweeps), a dispatch to `Explore` with a category-classification prompt collapses inventory cost. Explore's output is small (~60 one-line entries); avoids pulling 60 file fragments into the main thread's context. Required for wave shapes where inventory is non-uniform.
- **Shape-C peer-fed-to-API pattern needs `_asPeer()` at call, not facade.** Because `makePeerInfoController(peer: Peer)` / `FoundPeer(peer: Peer, ...)` / `SendAsPeer(peer: Peer, ...)` / `makeChatQrCodeScreen(peer: Peer, ...)` all stay on raw `Peer` (they're AccountContext-protocol or TelegramCore struct-init APIs whose migration is its own multi-wave effort), the bridge is a single `._asPeer()` at the call. Don't try to also migrate those APIs in the sweep — blast radius too large.
- **Engine-access varies by containing context.** Plain `context.engine.data` works for ~85% of sites; the remainder need `TelegramEngine(account: account)` construction or `engineData` protocol property. Build a per-site `context` type check into pre-flight for call-site categories where `AccountContext` isn't guaranteed.
Plan / record: no plan doc this wave — user specified the migration pattern directly; the Explore catalog + commit message captured decisions.
---
## Wave 34 outcome (2026-04-24)
`FoundPeer.peer: Peer → EnginePeer`. Public field-type migration on the struct in `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift`. Atomic 12-file commit `fdd5b93998`. ~135 insertions / ~134 deletions.
**Migration shape.** The field-type change is necessarily atomic (half-migrated FoundPeer doesn't compile across consumers), so all edits land in one commit. `_internal_searchPeers` keeps `import Postbox` (still calls `postbox.transaction` etc.) and wraps raw peer values with `EnginePeer(peer)` at the FoundPeer constructor sites. `==` body changes from `lhs.peer.isEqual(rhs.peer)` to `lhs.peer == rhs.peer`.
**Final scope (vs planned ~70 semantic edits → actual ~135 line insertions):**
- 5 `._asPeer()` bridge-drops at FoundPeer constructor sites (e.g., `FoundPeer(peer: peer._asPeer(), ...)``FoundPeer(peer: peer, ...)`)
- 22+ redundant `EnginePeer(peer.peer)` wrap drops (the field is now EnginePeer; `EnginePeer.init(_ peer: Peer)` doesn't accept an EnginePeer argument so the wrap fails to compile)
- 30+ Postbox-concrete-type downcasts (`peer.peer as? TelegramX` / `is TelegramX`) rewritten to `if case let .X(x) = peer.peer` enum-pattern form
- ~10 `._asPeer()` outflow bridges added where `peer.peer` flows into APIs that still take raw `Peer`: `ContactListPeer.peer(peer:)`, `canSendMessagesToPeer(_:)`, `EngineRenderedPeer(peer:)` legacy paths
**Inventory undercounting — pattern.** Original Explore inventory pass missed 4 of 12 final consumer files. The grep `grep -rln "FoundPeer\b"` only catches files that name `FoundPeer` as a literal type. Files that USE `peer.peer` access on FoundPeer values without naming the type itself were invisible to that grep. The build verification pass surfaced them:
| File | Surfaced by | Edits needed |
|---|---|---|
| `TelegramCore/Calls/GroupCalls.swift` | iter 1 | 2 internal FoundPeer constructors needed `EnginePeer(peer)` wraps |
| `ShareController/ShareSearchContainerNode.swift` | iter 2 | 4 errors: 2 C2 downcasts + 2 outflow-bridge needs |
| `ContactListUI/ContactsSearchContainerNode.swift` | iter 3 | 7 errors: nested `if !(peer is X)` rewrite + multiple downcasts/outflows |
| `PeerInfoUI/ChannelMembersSearchContainerNode.swift` | iter 4 | 6 errors across 2 near-identical loop blocks |
| `ChatListUI/ChatListSearchListPaneNode.swift` (extra site) | iter 5 | 1 missed C2 site at line 3723 (in `.globalPeer(foundPeer, …)` enum case body, far from the other ChatListUI edits) |
5 build iterations total before clean (each iteration: edit → re-build, ~5060s incremental). First-pass would have needed a much wider pre-flight grep — see lessons.
**Lessons:**
- **Inventory grep must include the access pattern, not just the type name.** For a field-type migration, ALL of:
- `<Type>(peer:` constructors
- `<x>.peer.<member>` reads (verify `<x>` type is `<Type>`, not RenderedPeer/SendAsPeer/etc.)
- `<x>.peer as?` / `<x>.peer is` downcasts
- `<api>(<x>.peer)` arg passes (where `<api>` may take the old protocol)
Use `for x in Y` binding-tracing to determine if `<x>` is the migrated type. The wave-34 pre-flight ran the first three but not the fourth (outflow-arg sites), and partially missed the second (because the Explore agent classified by literal `FoundPeer` token rather than by `peer.peer` semantics in context).
- **`if !(peer is A || peer is B)` rewrite uses `switch case A, B: break / default: ...`.** When the original Postbox code uses a negated disjunction of type-checks, the cleanest enum-pattern equivalent is a `switch` with combined cases in one arm — not nested `if case`s. (Used in ChatListSearchListPaneNode:1024 and ContactsSearchContainerNode:502/544.)
- **Inner `peer` shadowing.** Many `else if let peer = peer.peer as? TelegramChannel` Postbox patterns shadow the loop variable. The enum-pattern rewrite renames the inner binding to `channel` to avoid double-shadowing the EnginePeer outer loop var. Block-internal references to `.info` etc. then move from `peer.info` to `channel.info`.
- **Build iteration = inventory completion.** When the inventory undercounting becomes apparent (build surfaces 5+ unexpected sites), don't abandon — iterate. Each build is fast (~50s incremental) and each error is actionable (`error: cast from EnginePeer to unrelated type X always fails` → C2 rewrite; `argument type EnginePeer does not conform to expected type Peer` → outflow bridge). The inventory grows by file, fix-then-rebuild converges in 5 iterations even when ~30% of sites were missed up front.
- **Bridge sites generated by this wave point to next-ring migration targets.** The ~10 `._asPeer()` outflow bridges land at `ContactListPeer.peer(peer:)`, `canSendMessagesToPeer(_:)`, and `EngineRenderedPeer(peer:)` (legacy raw-Peer constructor in some paths — e.g., `EngineRenderedPeer(peer: foundPeer.peer)` doesn't need a bridge in newer EnginePeer-aware paths but does where the local var was already raw-Peer-extracted). These three signatures are the obvious wave-35+ candidates for the next ring of migration.
**Plan / record:** `docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-foundpeer-engine-peer-migration-design.md`.
---
## Wave 35 outcome (2026-04-24)
`SendAsPeer.peer: Peer → EnginePeer`. Public field-type migration on the struct in `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift`. Atomic 7-file commit `583c8b1f7c`. 22 insertions / 26 deletions.
**Migration shape.** Same atomic-field-type pattern as wave 34 but scoped to a smaller consumer surface. The `_internal_*SendAsAvailablePeers` functions keep `import Postbox` and wrap raw peer values with `EnginePeer(peer)` at the 4 SendAsPeer constructor sites. Manual `==` body dropped in favor of synthesized Equatable (`EnginePeer: Equatable`, `Int32?` and `Bool` already Equatable).
**Final scope (vs planned ~15 semantic edits → actual 22/26 line diff):**
- 3 `._asPeer()` bridge-drops at SendAsPeer constructor sites (ChatControllerLoadDisplayNode:772, ChatTextInputPanelComponent:848, StoryItemSetContainerViewSendMessage:249)
- 7 redundant `EnginePeer(peer.peer)` / `EnginePeer($0.peer)` / `EnginePeer(value.peer)` wrap drops across ChatSendAsPeerListContextItem (4 sites), ChatTextInputPanelNode (1), StoryItemSetContainerViewSendMessage (1), StoryItemSetContainerComponent (1)
- 1 `peer.peer as? TelegramChannel` downcast rewritten to `if case let .channel(channel) = peer.peer` (ChatSendAsPeerListContextItem:73) with `peer.info → channel.info` rename in the shadowed scope
- 2 `EnginePeer(channel)` wraps added where raw `TelegramChannel` is constructed into `SendAsPeer(peer: ...)` (ChatControllerLoadDisplayNode:805, 823)
- 1 signal-chain simplification: `(sendAsPeer?.peer).flatMap(EnginePeer.init)``sendAsPeer?.peer` at StoryItemSetContainerViewSendMessage:4080
- 1 signal-chain simplification: `.map({ EnginePeer($0.peer) })``.map({ $0.peer })` at StoryItemSetContainerViewSendMessage:4081
**Inventory undercount = 1 site (vs wave 34's 5).** The pre-flight Explore catalog missed `StoryItemSetContainerComponent.swift:3069` (`currentPeer: EnginePeer(value.peer)``value.peer`). The implementer caught it during the edit phase before the build, so no iteration was needed. The wave-34 explicit pattern grep (including `.peer as?`/`is`/outflow-args/`EnginePeer(.peer)`/`._asPeer()`) dramatically reduced undercounting — 1/7 sites missed (~14%) vs wave 34's 4/12 (~33%).
**First-pass clean build.** No errors surfaced by the Bazel build at all. 461 total actions, 196.583s elapsed, `INFO: Build completed successfully`. Contrast with wave 34's 5 build-iterations-to-converge.
**Lessons:**
- **Wave 34's explicit-pattern pre-flight inventory works.** For future Peer-typed-API waves, the minimum grep pattern set is: `<Type>\b` literal token, `\.<fieldName>\s+(as\?|is)\s+Telegram`, `EnginePeer\(\w+\.<fieldName>\)`, `<api>\(<x>\.<fieldName>` for known outflow APIs, and `\._asPeer\(\)` (to catch bridge-drop opportunities). Wave 35 used this full pattern set and hit ~14% undercount vs wave 34's ~33%.
- **Smaller target + validated pattern = faster wave.** Wave 35 went from spec-commit (`72d4384af0`) to outcome-commit in a single session with one clean build, versus wave 34's multi-iteration convergence. When the wave is a replay of a just-validated pattern on a smaller surface, expect minimal iteration.
- **Inner-peer shadowing rename works.** The wave-34 lesson about renaming `peer``channel` in `if case let .channel(channel) = peer.peer` applied cleanly. Single instance this wave (ChatSendAsPeerListContextItem:73) — no issues.
- **Name collisions remain a scope hazard.** Pre-flight identified `sendAsPeers: [EnginePeer]` (LiveStreamSettingsScreen, ShareWithPeersScreen) and `availableSendAsPeers: [EnginePeer]` (ChatSendStarsScreen) as name-only collisions — different type, same identifier. Confirmed these stayed untouched and out of scope. Future Peer-typed-API waves should continue the name-collision disambiguation pass.
- **Bridge sites generated by this wave — zero new outflow bridges.** Unlike wave 34 (which added ~10 `._asPeer()` outflow bridges pointing to `ContactListPeer.peer(peer:)` / `canSendMessagesToPeer(_:)` / `EngineRenderedPeer(peer:)` as next-ring targets), wave 35 added no outflow bridges. All consumer-side `.peer` flows either stayed as `.peer.id` accesses (PeerId unchanged) or were simplifications of existing `EnginePeer(.peer)` wraps. Net: no new next-ring targets surfaced from wave 35.
**Plan / record:** `docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md`.
---
## Wave 36 outcome (2026-04-24)
`ContactListPeer.peer(peer: Peer, isGlobal:, participantCount:) → peer: EnginePeer`. Enum-case payload migration on the public type in `submodules/AccountContext/Sources/ContactSelectionController.swift`. Atomic 15-file commit `069a060de1`. 57 insertions / 59 deletions.
**Migration shape.** Same atomic-payload-type pattern as wave 34/35 but wider: 15 consumer files vs wave 35's 7, vs wave 34's 12. Beyond the payload change, the cascading `ContactListPeer.indexName` return type changed from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` — an unexpected discovery during plan-writing that dropped 2 additional `EnginePeer.IndexName(...)` wraps at ContactListNode:517.
**Final scope (vs planned 8 files / ~41 semantic edits → actual 15 files / 57/59 diff):**
- **Definition (1 file):** `AccountContext/ContactSelectionController.swift` — case payload type, indexName return type, `==` operator body (`lhsPeer.isEqual(rhsPeer)``lhsPeer == rhsPeer`).
- **20 `._asPeer()` outflow bridge-drops** across ContactListNode (12), ContactsSearchContainerNode (3), ContactMultiselectionController (2), ContactMultiselectionControllerNode (1), ContactSelectionControllerNode (2). `replace_all=true` on `._asPeer(), isGlobal:` was the unifying substring.
- **20+ `EnginePeer(peer)` inflow wrap-drops** at destructure sites across ContactListNode (4), ContactsController (1), ContactsSearchContainerNode (4), ContactMultiselectionController (4), ContactMultiselectionControllerNode (1), ContactSelectionController (2), PeerSelectionControllerNode (3), SharedAccountContext (2).
- **2 `EnginePeer.IndexName(...)` wrap-drops** at the sort-comparator at ContactListNode:517 (enabled by the cascading return-type change).
- **8 Postbox-concrete cast rewrites** to EnginePeer case patterns across ContactListNode:182-186/1968 (4 sites, including the 3-branch user/group/channel cast-chain), CallController:524/542 (the intermediate `let peer = EnginePeer(peer)` lines became redundant after migration), StoryItemSetContainerViewSendMessage:2041/2074, DeviceContactInfoController:1419, ChatSendAudioMessageContextPreview:89, ChatControllerOpenAttachmentMenu:557/610/1746/1788 (4 identical sites, `replace_all` on the full line).
- **2 `._asPeer()` outflow bridges ADDED** at ContactMultiselectionController:386/403 where the destructured peer flows into `peerTokenTitle(peer: Peer)` (out-of-scope callee; future-wave bridge target).
**Inventory undercount = 7 files / ~20 sites (vs wave 35's 1 site).** Much higher miss rate than wave 35 — ~46% by file count. Root cause: the pre-flight grep for ContactListPeer destructures used literal `\(peer, _, _\)` binding; binding names varied in practice (`contact`, `lhsPeer`, `rhsPeer`, `contactPeer`, `id`). Files missed:
1. `DeviceContactInfoController.swift:1418/1419``case let .peer(contact, _, _)` + `contact as? TelegramUser`
2. `CallController.swift:523/541``case let .peer(peer, _, _)` + redundant `let peer = EnginePeer(peer)` pattern
3. `ChatSendAudioMessageContextPreview.swift:88/89``case let .peer(contact, _, _)` + `contact as? TelegramUser`
4. `PeerSelectionControllerNode.swift:901-903/1590-1592` — 2 destructures with `EnginePeer(peer)` inflow wraps
5. `StoryItemSetContainerViewSendMessage.swift:2040-2041/2073-2074` — 2 `contact as? TelegramUser` casts
6. `ChatControllerOpenAttachmentMenu.swift:556-1787` — 4 `contact as? TelegramUser` casts
7. `SharedAccountContext.swift:3295-3302``case let .peer(peer, _, _)` + 2 `EnginePeer(peer)` inflow wraps
**Six build iterations to converge** vs wave 35's single first-pass-clean. Iterations 1-6 surfaced errors in batches of 2-8 errors; each was a mechanical fix (drop wrap, rewrite cast, add `._asPeer()` bridge for outflow to out-of-scope `peerTokenTitle`). Final iteration (#6) clean.
**Lessons:**
- **Pre-flight grep must use `\(\w+, _, _\)` not `\(peer, _, _\)` for enum-payload destructures.** Swift destructure patterns bind the payload to any legal identifier; the variable name is not semantic. Future Peer-typed-enum-payload waves should use `case let \.<caseName>\((\w+),` (or similar wildcard binding) and then per-destructure scan the next ~15 lines for `<binding> as\?`/`<binding> is`/`EnginePeer\(<binding>\)` / outflow-arg patterns.
- **"No-edit consumer" claims need stricter verification.** Wave 36's "verify-only" list included ChatSendAudioMessageContextPreview because the initial inventory found only `[ContactListPeer]` at collection level. The deeper scan missed a `case let .peer(contact, _, _)` + `contact as? TelegramUser` pattern inside the file's `update(...)` method. For future waves, "no-edit" claims should run the wildcard-binding destructure grep described above, not just a construction-site grep.
- **Outflow-to-out-of-scope-API bridges may need addition during the wave.** ContactMultiselectionController:386/403 needed `._asPeer()` bridges added where none existed pre-migration — the pre-migration code passed raw `Peer` to `peerTokenTitle(peer: Peer)` because the destructured peer was raw Peer. Post-migration, the destructured peer is EnginePeer, so a bridge is required. Future waves with same-scope outflow to not-yet-migrated Peer-typed APIs should pre-flight expect to add bridges.
- **Cascading computed-property return type migration** (here: `ContactListPeer.indexName` from `PeerIndexNameRepresentation` to `EnginePeer.IndexName`) is a legitimate scope expansion when the enum's properties leak Postbox-typed values. Wave 36 caught this during plan-writing, not execution — a successful plan-review win. Future waves should grep the enum's definition file for computed properties returning Postbox-defined types.
- **Build-iteration convergence is acceptable** when the wave's surface is large and pre-flight undercount is non-trivial. The cost of 6 build iterations (~5-20 minutes each in the Telegram-iOS build) is real but manageable. The alternative — exhaustive pre-flight to achieve first-pass-clean — is more expensive in plan-writing tokens and controller wall time. For waves expected to have >5 file touches, plan should explicitly budget for 3-5 build iterations.
- **Ratchet effect confirmed.** Wave 36 was predominantly bridge-removal (20 outflow + 20 inflow + 2 IndexName) with only 2 bridge additions. Matches the expected ratchet behavior: earlier waves 33/34/35 added bridges at Peer/EnginePeer boundaries precisely so wave 36 could drop them atomically. The 2 new bridges added (ContactMultiselectionController:386/403 → peerTokenTitle) become next-wave drop candidates once `peerTokenTitle(peer: Peer)` migrates.
**Plan / record:** `docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md`.
---
## Modules currently free of `import Postbox` (running tally)

View file

@ -0,0 +1,227 @@
# Wave 36 — `ContactListPeer.peer` `Peer``EnginePeer`
Date: 2026-04-24
Status: approved design, awaiting plan
Wave shape: Peer-typed-API enum-case payload migration, single atomic commit (waves 34/35 pattern)
## Goal
Eliminate the Postbox-protocol `Peer` leak in the `ContactListPeer.peer(peer:isGlobal:participantCount:)` case payload by migrating the `peer` field from `Peer` to `EnginePeer`. Drop the outflow `._asPeer()` bridges that waves 33/34 installed at construction sites, and the inflow `EnginePeer(...)` wrappings at destructure sites. Apply wave 35's validated pre-flight pattern set (literal token + `.peer as?`/`is` + outflow-args + `EnginePeer(.peer)` + `._asPeer()`) to keep undercount below wave 35's 14%.
## Non-goals
- `ContactListPeerId.peer(PeerId)` (sibling enum, different payload) — unchanged; `PeerId == EnginePeer.Id` makes it already-clean.
- `canSendMessagesToPeer(_ peer: Peer, ignoreDefault: Bool) -> Bool` parameter migration — broader blast radius, deferred.
- `makePeerInfoController` / `makeChatQrCodeScreen` / `makeChatRecentActionsController` protocol-method migrations — broader blast radius, deferred.
- `openPeer(peer: Peer, ...)` / other Peer-typed APIs called from destructured bodies — if any destructured `peer` outflows into a raw-`Peer`-typed API after migration, add a `._asPeer()` bridge at that call site. Migrating those APIs is its own future wave.
- No new engine wrappers, typealiases, or facades introduced in this wave.
- No `import Postbox` drops in this wave — deferred to a follow-on unused-import sweep.
## Type change
```swift
// Before
public enum ContactListPeer: Equatable {
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId { … }
public var indexName: PeerIndexNameRepresentation { … }
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs,
lhsPeer.isEqual(rhsPeer), // Postbox protocol method
lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else { return false }
case let .deviceContact(id, contact):
if case .deviceContact(id, contact) = rhs { return true } else { return false }
}
}
}
// After
public enum ContactListPeer: Equatable {
case peer(peer: EnginePeer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId { … } // body unchanged; peer.id is EnginePeer.Id == PeerId
public var indexName: EnginePeer.IndexName { … } // return type changed — body unchanged but type flows from EnginePeer.indexName
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs,
lhsPeer == rhsPeer, // EnginePeer is Equatable
lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else { return false }
case let .deviceContact(id, contact):
if case .deviceContact(id, contact) = rhs { return true } else { return false }
}
}
}
```
The custom `==` is retained (rather than relying on synthesis) because `DeviceContactStableId` / `DeviceContactBasicData` conformance to Equatable is not verified here; minimising unrelated change. Only the `lhsPeer.isEqual(rhsPeer)` clause is rewritten.
## In-scope files
Scope based on the pre-flight Explore inventory plus a manual deep-scan pass that caught additional inflow wraps and Postbox-concrete casts the Explore agent missed. One definition file plus nine consumer files; seven of the consumer files need edits. Two (ComposeController, ChatSendAudioMessageContextPreview) have only `.id`-level accesses and should need no body change — plan verifies each during implementation.
### Category α — Definition (`AccountContext`)
**`submodules/AccountContext/Sources/ContactSelectionController.swift`**
- Line 62: enum case signature change `peer: Peer``peer: EnginePeer`.
- Line 74: computed property return type change `PeerIndexNameRepresentation``EnginePeer.IndexName`. Rationale: after the payload migration, `peer.indexName` at line 77 returns `EnginePeer.IndexName` (from `EnginePeer.indexName`), not `PeerIndexNameRepresentation`. Changing the return type up rather than re-bridging via `peer._asPeer().indexName` eliminates a Postbox-typed API from AccountContext and incidentally lets two `EnginePeer.IndexName(...)` wraps at ContactListNode:517 drop. The two enum case shapes match exactly — `EnginePeer.IndexName.title(title:addressNames:)` and `EnginePeer.IndexName.personName(first:last:addressNames:phoneNumber:)` are defined at `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:145-147` with the same parameter labels and types as `PeerIndexNameRepresentation`'s cases.
- Line 77: `return peer.indexName` — body unchanged; type now flows `EnginePeer → EnginePeer.IndexName`.
- Line 79: `return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")` — body unchanged; case resolution retargets to `EnginePeer.IndexName.personName`.
- Line 86: `==` operator — rewrite `lhsPeer.isEqual(rhsPeer)` to `lhsPeer == rhsPeer`.
- Line 67: `peer.id` same-type access (EnginePeer.id returns EnginePeer.Id ≡ PeerId) — unchanged.
### Category β — Outflow-bridge drops (the dominant pattern)
Every site below is `.peer(peer: <expr>._asPeer(), isGlobal: …, participantCount: …)``.peer(peer: <expr>, …)`, because `<expr>` is already `EnginePeer` at the call site.
**`submodules/ContactListUI/Sources/ContactListNode.swift`** — 12 sites: 632, 690, 701, 747, 765, 1365, 1647, 1656, 1693, 1731, 1942, 1944.
**`submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift`** — 3 sites: 494, 535, 569.
**`submodules/TelegramUI/Sources/ContactMultiselectionController.swift`** — 2 bridged sites: 451, 459.
**`submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift`** — 1 site: 317.
**`submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift`** — 2 sites: 160, 230.
Total: 20 outflow-bridge drops.
### Category γ — Removed
Earlier draft flagged `TelegramUI/ContactMultiselectionController.swift:379` as a raw-`Peer` construction needing `EnginePeer(peer)` promotion. Rechecked: line 379 is inside a destructure at line 347 (`case let .peer(peer, _, _) = peer`), so post-migration the inner `peer` is already `EnginePeer` and the existing `.peer(peer: peer, ...)` continues to compile without wrapping. No edit needed.
### Category δ — Inflow-wrapping drops at destructure sites
Every site is `EnginePeer(peer)` applied to a destructured peer that becomes `EnginePeer` directly post-migration → drop each wrap.
- **ContactListNode.swift**: 4 wraps total.
- Line 204 wraps `peer` twice inside `.peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))` (inside destructure at line 177).
- Line 252 wraps once inside `interaction.openDisabledPeer(EnginePeer(peer), …)` (inside destructure at line 251).
- Line 844 wraps once inside `isPeerEnabled(EnginePeer(peer))` (inside destructure at line 833).
- **ContactsController.swift**: 1 wrap — line 294 `chatLocation: .peer(EnginePeer(peer))` where `peer` is destructured at line 287.
- **ContactsSearchContainerNode.swift**: 4 wraps total.
- Line 164 `peerItem = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))` (2 wraps, inside destructure at line 163).
- Line 165 `nativePeer = EnginePeer(peer)` (1 wrap, same destructure).
- Line 181 `openDisabledPeer(EnginePeer(peer), …)` (1 wrap, inside destructure at line 180).
- **TelegramUI/Sources/ContactMultiselectionController.swift**: 4 wraps total.
- Line 386 `subject: .peer(EnginePeer(peer))` (inside destructure at line 347).
- Line 403 `subject: .peer(EnginePeer(peer))` (same destructure).
- Line 481 `self.params.sendMessage?(EnginePeer(peer))` (inside destructure at line 468).
- Line 491 `self.params.openProfile?(EnginePeer(peer))` (same destructure).
- **TelegramUI/Sources/ContactMultiselectionControllerNode.swift**: 1 wrap — line 492 `EnginePeer(peer).compactDisplayTitle` (inside destructure at line 491).
- **TelegramUI/Sources/ContactSelectionController.swift**: 2 wraps total.
- Line 517 `self.sendMessage?(EnginePeer(peer))` (inside destructure at line 504).
- Line 527 `self.openProfile?(EnginePeer(peer))` (same destructure).
Total: 16 inflow-wrap drops.
### Category φ — Postbox-concrete cast rewrites
Destructured `peer` post-migration is `EnginePeer`. Existing `peer as? TelegramUser`/`TelegramGroup`/`TelegramChannel` casts no longer compile; rewrite to `EnginePeer` case-pattern matches. Both sites are in `ContactListNode.swift`.
- **ContactListNode.swift:182-186** — inside destructure at line 177. Rewrite the `if let _ = peer as? TelegramUser { … } else if let group = peer as? TelegramGroup { … } else if let channel = peer as? TelegramChannel { … }` chain to `switch peer { case .user: … case let .legacyGroup(group): … case let .channel(channel): … default: break }`, or equivalently to the `if case .user = peer / if case let .legacyGroup(group) = peer / if case let .channel(channel) = peer` chain. Inner `group.participantCount`, `channel.info`, `case .group = channel.info` continue to compile unchanged because `EnginePeer.channel` / `.legacyGroup` wrap the exact same concrete types (`TelegramChannel`, `TelegramGroup`) and `.user` wraps `TelegramUser`. Note: the original `if let _ = peer as? TelegramUser` branch doesn't bind the user — rewrite keeps that (either `case .user = peer` or `if case .user = peer`).
- **ContactListNode.swift:1968** — inside destructure at line 1966. Rewrite `let user = peer as? TelegramUser` to `case let .user(user) = peer`. Inner `user.phone` continues to compile (`EnginePeer.user` wraps `TelegramUser`).
EnginePeer enum case mapping (reference):
| Postbox concrete | EnginePeer case |
|---|---|
| `TelegramUser` | `.user(TelegramUser)` |
| `TelegramGroup` | `.legacyGroup(TelegramGroup)` |
| `TelegramChannel` | `.channel(TelegramChannel)` |
Lines 1802, 1818, 1820 in ContactListNode.swift also contain `peer as? TelegramChannel`/`peer is TelegramGroup` casts but these are on `peer` values sourced from `entryData.renderedPeer.peer` (raw Postbox `Peer`), not from a ContactListPeer destructure. They stay unchanged — out of wave scope.
### Category ε′ — `ContactListPeer.indexName` return-type cascade
Because category α changes the return type of `ContactListPeer.indexName` to `EnginePeer.IndexName`, call sites that currently wrap that return in `EnginePeer.IndexName(...)` can drop the wrap:
- **ContactListNode.swift:517**`let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder)``let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder)`. Two wraps drop. The `isLessThan(other:ordering:)` extension is defined on `EnginePeer.IndexName` only (see `submodules/LocalizedPeerData/Sources/PeerTitle.swift:64`), so the existing wrap idiom was required pre-migration.
- **ContactListNode.swift:539, 590**`switch peer.indexName` / `switch orderedPeers[i].indexName` with `case let .title(…)` and `case let .personName(…)` — continues to compile unchanged. Same case names and shapes.
### Category ε — Same-type field access (no edit)
Destructured peer bindings whose only uses are `.id`, `.addressName`, value equality via `.id`, etc. All of these exist on `EnginePeer` with identical semantics.
Known sites from inventory (accept as same-type):
- **ContactSelectionController.swift**: 67, 76 — `.id`, `.indexName`.
- **ContactListNode.swift**: 121, 177, 209, 216, 251, 255, 491, 505, 519, 520, 782, 787, 827, 833, 1636, 1966 — `.id`/`.addressName`/value comparisons on `.id`. Sites 204 and 251 also appear in category δ because the same binding is used both ways in the same block.
- **ContactsSearchContainerNode.swift**: 151 — `.addressName`.
- **ContactMultiselectionController.swift**: 347, 468 — `.id`.
- **ContactMultiselectionControllerNode.swift**: 491 — `selectedPeers.first` destructure to access `.id`.
- **ContactSelectionController.swift (TelegramUI)**: 504 — context-action passthrough.
- **ComposeController.swift**: 120, 160 — `.id` for chat creation.
- **ChatSendAudioMessageContextPreview.swift**: 88 — `.contact`/name accessors.
These need no code edits; they are listed only to record coverage.
### Category ζ — Outflow-to-`Peer`-typed-API (bridge required)
Any destructured `peer` (now `EnginePeer`) passed to a function that takes raw `Peer` needs `._asPeer()` appended at the call site.
Known candidate from inventory:
- **ContactsSearchContainerNode.swift:180**`isPeerEnabled(peer)`. Verify the parameter type at edit time. If it is `(EnginePeer) -> Bool`, no bridge needed; if `(ContactListPeer) -> Bool`, also no bridge (the destructured value is discarded for the overall `peer` value anyway). If `(Peer) -> Bool`, add `._asPeer()`.
Plan-time step 7 verifies each category-ε site against the API it feeds into; any surprise is resolved by adding `._asPeer()` inline.
## Out-of-scope — name collisions
Files listed in the 20-file grep but not touched in this wave:
- **PeerInfoUI/ChannelMembersController.swift**, **PeerInfoUI/ChannelVisibilityController.swift**, **SettingsUI/…/GlobalAutoremoveScreen.swift**, **IncomingMessagePrivacyScreen.swift**, **SelectivePrivacySettingsController.swift**, **SelectivePrivacySettingsPeersController.swift**, **PresentAddMembers.swift**, **ComposeController.swift (TelegramUI)**, **OpenResolvedUrl.swift**, **ChatSendAudioMessageContextPreview.swift** — the inventory found only `ContactListPeerId.peer(…)` destructures or pass-throughs of the entire `ContactListPeer` enum value, not `ContactListPeer.peer` payload access. The payload-type migration does not affect these.
Plan-time verification: re-grep these files for `case .peer(let peer`, `case let .peer(peer,`, and `.peer(peer:` before declaring "no edits needed". If a missed payload destructure surfaces, promote the file into scope.
## Execution plan outline (for writing-plans)
Single atomic commit ordering:
1. Edit `AccountContext/ContactSelectionController.swift` — change case payload type (L62); change `indexName` property return type to `EnginePeer.IndexName` (L74); rewrite `lhsPeer.isEqual(rhsPeer)` to `lhsPeer == rhsPeer` (L86).
2. Edit `ContactListNode.swift` — drop 12 `._asPeer()` bridges (outflow); drop 4 inflow `EnginePeer(peer)` wraps (2 on L204, 1 on L252, 1 on L844); rewrite cast chain at L182-186 to EnginePeer case patterns; rewrite cast at L1968; drop 2 `EnginePeer.IndexName(...)` wraps on L517.
3. Edit `ContactsController.swift` — drop 1 inflow `EnginePeer(peer)` wrap at L294.
4. Edit `ContactsSearchContainerNode.swift` — drop 3 `._asPeer()` bridges at L494/535/569; drop 4 inflow `EnginePeer(peer)` wraps (2 on L164, 1 on L165, 1 on L181). Do NOT drop `._asPeer()` at L488/528/562 (these feed `canSendMessagesToPeer(_: Peer)` — deferred wave).
5. Edit `TelegramUI/ContactMultiselectionController.swift` — drop 2 outflow bridges at L451/459; drop 4 inflow wraps at L386/403/481/491. Do NOT edit L171/201/748 (these feed `peerTokenTitle(peer: Peer)` — deferred).
6. Edit `TelegramUI/ContactMultiselectionControllerNode.swift` — drop 1 outflow bridge at L317; drop 1 inflow wrap at L492.
7. Edit `TelegramUI/ContactSelectionController.swift` — drop 2 inflow wraps at L517/527.
8. Edit `TelegramUI/ContactSelectionControllerNode.swift` — drop 2 outflow bridges at L160/230.
9. Verify `ComposeController.swift` and `ChatSendAudioMessageContextPreview.swift` need no body edits. If build surfaces a leak, fold the fix into an additional task step.
10. Build: `source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError`.
11. Address undercount misses (expected ≤3 — pre-flight was thorough but file count is large) and commit once build is green.
## Risk register
| Risk | Mitigation |
|------|------------|
| Inventory undercount (wave 35 had 14%; trend decreasing) | Pre-flight already uses validated pattern set. `--continueOnError` on the build surfaces all misses in one pass. Expected ≤2 missed sites. |
| Destructure sites that flow a peer into a raw-`Peer`-typed API (category ζ) not caught by inventory | Build will flag the type mismatch; fix inline with `._asPeer()` at the flagged call site. Plan step 8 is the explicit verification gate. |
| `ContactListPeer` Equatable semantic regression | Replacing `lhsPeer.isEqual(rhsPeer)` (Postbox dynamic dispatch) with `lhsPeer == rhsPeer` (EnginePeer synthesized `==`) compares the same underlying concrete types (`.user(TelegramUser)`, `.channel(TelegramChannel)`, etc.) via their own Equatable conformances. Truth table preserved. |
| `ContactListPeer.indexName` return-type change cascades beyond ContactListNode:517/539/590 | Consumers of `ContactListPeer.indexName` enumerated via `grep -rn "\.indexName" submodules/ --include="*.swift"` filtered for ContactListPeer-typed receivers: only ContactListNode has such uses. No other submodule destructures or pattern-matches on this property. Build will flag any miss immediately. |
| `peer.isEqual` used elsewhere in scope files but on non-ContactListPeer bindings | Inventory confirmed ContactListNode:306 uses `!=` on a `ContactListNodeEntry.peer` binding, not `ContactListPeer.peer`. Scope boundary respected. No other `isEqual` call on a ContactListPeer-destructured binding was found. |
| Files flagged "no ContactListPeer.peer payload access" turn out to have one | Plan step 8 re-greps these files; any hit gets promoted into scope without rerunning the wave. |
| Pre-existing WIP on `ChatListFilterPresetController.swift` / `ChatListFilterPresetListController.swift` | Out of wave scope — untouched. No ContactListPeer reference expected in those files. |
## Validation
- Full Bazel build (`--configuration=debug_sim_arm64 --continueOnError`).
- No TelegramCore/Postbox/TelegramApi errors (scope boundary check — halt if they surface).
- Grep post-commit: `rg "ContactListPeer\.peer\(peer: .*\._asPeer" submodules/` returns empty.
- Grep post-commit: `rg "case \.peer\(peer: .*\._asPeer" submodules/` returns empty (catch shortcut constructions).
- Grep post-commit: no surviving `EnginePeer\(peer\)` in the 10 touched files where `peer` was destructured from a `ContactListPeer.peer` case (manual spot-check — automated grep too noisy).
## Lessons to carry forward
- Wave 35's pre-flight pattern set (literal token + `.peer as?`/`is` + outflow-args + `EnginePeer(.peer)` + `._asPeer()`) applied to this wave; record the post-commit undercount percentage to continue the calibration trend (wave 34: ~33%, wave 35: ~14%).
- This wave is dominated by **bridge removal** — 20 outflow `._asPeer()` drops + 16 inflow `EnginePeer(peer)` drops + 2 `EnginePeer.IndexName(...)` drops + 1 `.isEqual``==` fix + 2 Postbox-cast chain rewrites. Zero bridge additions. Updated tallies supersede earlier draft counts in this spec. Confirms the ratchet effect: earlier waves added bridges at Peer/EnginePeer boundaries precisely so future waves like this one can drop them atomically. Record the ratio (bridge drops : bridge additions) as a health metric across Peer-typed-API waves.
- Custom enum `==` operators using `Peer.isEqual(_:)` are a predictable Category-F leak in every Peer-payload migration. Future Peer-typed-API waves should grep the enum's defining module for `\.isEqual\(` specifically.
- **Computed properties on the enum that return Postbox types (e.g., `PeerIndexNameRepresentation`) are a second predictable leak** — discovered mid-spec for `ContactListPeer.indexName`. Future Peer-typed-enum waves should grep the enum's definition file for `public var` / `public func` returning any Postbox-defined type (`PeerIndexNameRepresentation`, `PeerNameIndex`, `MessageId`, etc.) before committing to the inventory — changing the return type to the Engine equivalent frequently cascades into consumer-side wrap drops (here, 2 wraps at ContactListNode:517).

View file

@ -0,0 +1,193 @@
# Wave 34 Design: `FoundPeer.peer: Peer → EnginePeer`
**Date:** 2026-04-24
**Wave:** 34 (Postbox → TelegramEngine refactor)
**Predecessor:** Wave 33 (loadedPeerWithId consumer sweep, commit `16d017853a`)
## Goal
Migrate the public field `FoundPeer.peer` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum. Drops 4 of the 5 `._asPeer()` bridges introduced by wave 33 and eliminates one Postbox-protocol leak from a `TelegramEngine.Contacts` / `TelegramEngine.Calls` return type.
## Non-Goals
- Migrating other Peer-typed-API surfaces (`SendAsPeer`, `makePeerInfoController`, `makeChatRecentActionsController`, `makeChatQrCodeScreen`, `FoundPeer` is the smallest probe in this class — those are separate future waves).
- Dropping `import Postbox` from `SearchPeers.swift`. The `_internal_*` functions in that file still call `postbox.transaction`, `parseTelegramGroupOrChannel`, `AccumulatedPeers`, `updatePeers`. They remain the Postbox-facing layer per project rule.
- Dropping `import Postbox` from any consumer module. None of the touched files reach zero Postbox use through this change alone.
- Auto-synthesizing `Equatable` for `FoundPeer`. Manual `==` is preserved per user decision.
## Scope
One atomic commit. Approximately 46 semantic edits plus type-name continuations across:
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift` (definition + `_internal_searchPeers` body)
- 7 consumer files in `submodules/`:
- `submodules/TelegramCallsUI/Sources/VideoChatScreen.swift`
- `submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift`
- `submodules/ContactListUI/Sources/ContactListNode.swift`
- `submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift`
- `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift`
- `submodules/TelegramBaseController/Sources/TelegramBaseController.swift`
- `submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift`
The remaining ~10 files identified by `grep -rln "FoundPeer\b"` (StorageUsageExceptionsScreen field-only refs aside, the file IS in the touched list above) contain only C5 type-name mentions or unrelated `.peer.peer` accesses on other types and require no edit.
**Verification (performed 2026-04-24)** that nearby `EnginePeer(peer.peer)` patterns in other files are NOT FoundPeer access: those sites bind `peer` to `SelectivePrivacyPeer`, `SendAsPeer`, `InactiveChannel`, `RenderedChannelParticipant`, or `RenderedPeer` — all of which still expose `.peer: Peer`. They remain unchanged by this wave.
## Changes
### 1. `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift`
**Struct:**
```swift
public struct FoundPeer: Equatable {
public let peer: EnginePeer // was: Peer
public let subscribers: Int32?
public init(peer: EnginePeer, subscribers: Int32?) { // was: peer: Peer
self.peer = peer
self.subscribers = subscribers
}
public static func ==(lhs: FoundPeer, rhs: FoundPeer) -> Bool {
return lhs.peer == rhs.peer && lhs.subscribers == rhs.subscribers
// was: lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers
}
}
```
**`_internal_searchPeers` body changes:**
- All four `FoundPeer(peer: peer, subscribers: …)` constructions (lines 70, 72, 85, 87) wrap the raw `peer` value with `EnginePeer(peer)`.
- Six scope-filter expressions (2 per non-trivial scope × 3 scopes — `.channels` lines 96109, `.groups` lines 110128, `.privateChats` lines 129143) rewrite to enum pattern matching:
- `as? TelegramChannel, case .broadcast = channel.info``if case let .channel(channel) = item.peer, case .broadcast = channel.info`
- `as? TelegramChannel, case .group = channel.info` plus `else if item.peer is TelegramGroup``if case let .channel(channel) = item.peer, case .group = channel.info` plus `else if case .legacyGroup = item.peer`
- `if item.peer is TelegramUser``if case .user = item.peer`
Filter behavior is preserved exactly; only the destructuring form changes.
### 2. Consumer-side edits (by category)
Inventory was performed on 2026-04-24 via Explore agent against the 10 files identified by `grep -rln "FoundPeer\b" submodules/ Telegram/`. An additional 3 files surfaced (`ShareControllerNode.swift`, `SharePeersContainerNode.swift`, `PeerSelectionControllerNode.swift`, `ContactSelectionControllerNode.swift`, `ChatListNode.swift`) — most are C5 type-name mentions or false positives in field names that don't reference the type.
**C1 — peer-protocol method reads (~28 sites): no edit required.**
`peer.peer.id`, `peer.peer.displayTitle`, `peer.peer.namespace`, `peer.peer.debugDisplayTitle`, `peer.peer.smallProfileImage` — all available on `EnginePeer` with the same signatures.
**C5 — type-signature mentions (~60 sites): no edit required.**
`[FoundPeer]`, `Signal<([FoundPeer], [FoundPeer]), NoError>`, `Atomic<([FoundPeer], [FoundPeer])?>`, `case globalPeer(FoundPeer, …)`, etc. The type continues to compile under the new field.
**C2 — downcast rewrites (30 sites).**
EnginePeer is an enum, so `peer.peer as? TelegramX` / `peer.peer is TelegramX` patterns must rewrite to `if case .X = peer.peer` (or `if case let .X(x) = peer.peer` when the bound value is reused). Case mapping:
- `TelegramUser``.user`
- `TelegramSecretChat``.secretChat`
- `TelegramGroup``.legacyGroup`
- `TelegramChannel``.channel`
| File | Line | Current pattern | After (representative) |
|---|---|---|---|
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 628 | `peer.peer is TelegramGroup` | `if case .legacyGroup = peer.peer` |
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 631 | `as? TelegramChannel, case .group = peer.info` | `if case let .channel(channel) = peer.peer, case .group = channel.info` |
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 648 | `as? TelegramChannel, case .broadcast = peer.info` | `if case let .channel(channel) = peer.peer, case .broadcast = channel.info` |
| `ContactListUI/ContactListNode.swift` | 1501 | `if let _ = peer.peer as? TelegramChannel` | `if case .channel = peer.peer` |
| `ContactListUI/ContactListNode.swift` | 1563, 1569, 1574 | `if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium)` | `if case let .user(user) = peer.peer, user.flags.contains(.requirePremium)` |
| `ContactListUI/ContactListNode.swift` | 1658, 1665, 1695, 1703, 1733 | `let user = peer.peer as? TelegramUser` (in if-let chains) | `if case let .user(user) = peer.peer, …` |
| `ContactListUI/ContactListNode.swift` | 1673, 1711 | `if peer.peer is TelegramGroup` (with possible `&& <bool>`) | `if case .legacyGroup = peer.peer` (with `, <bool>`) |
| `ContactListUI/ContactListNode.swift` | 1675, 1713 | `else if let channel = peer.peer as? TelegramChannel` | `else if case let .channel(channel) = peer.peer` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1024 | `!(peer.peer is TelegramUser \|\| peer.peer is TelegramSecretChat)` | rewrite to combined enum-pattern (×2 within the line) |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1029, 1030 | `if let _ = peer.peer as? TelegramGroup` / `else if let peer = peer.peer as? TelegramChannel, case .group = peer.info` | `if case .legacyGroup = peer.peer` / `else if case let .channel(channel) = peer.peer, case .group = channel.info` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1038, 1040 | `if peer.peer is TelegramUser` / `else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info` | `if case .user = peer.peer` / `else if case let .channel(channel) = peer.peer, case .broadcast = channel.info` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1500, 1507 | `if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info` | `if case let .channel(channel) = peer.peer, case .broadcast = channel.info` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 175, 178, 193 | (see prior lines, same pattern set) | (same) |
| `TelegramBaseController/TelegramBaseController.swift` | 243, 246, 258 | `peer.peer is TelegramGroup` / `as? TelegramChannel, case .group = peer.info` / `as? TelegramChannel, case .broadcast = peer.info` | (same enum-pattern rewrites as above) |
Two name-shadowing notes:
- **Inner `peer` shadowing.** Several rewrites (e.g., `else if let peer = peer.peer as? TelegramChannel`) currently shadow the loop variable with a new `peer` of type `TelegramChannel`. After rewrite these become `else if case let .channel(channel) = peer.peer` — the binding name moves from `peer` to `channel` to avoid further shadowing of the EnginePeer loop variable. Adjust subsequent body references inside the if-let scope (they currently say `peer.info` referring to `TelegramChannel.info`; they become `channel.info`). Spot-check each rewrite within its block.
- **`channel.info` references.** When a downcast block uses the bound `peer` for `.info` access (e.g., line 178: `peer.info`), update those references to use the new binding name (`channel.info`). Block-internal-only — no cascade.
Plus 6 filter sites inside `SearchPeers.swift` `_internal_searchPeers` body (already counted under §1).
**C4 — constructor edits (6 sites):**
Bridge-drop sites — wave-33 added `._asPeer()` because the value was already `EnginePeer`; with this wave the field accepts EnginePeer directly:
| File | Line | Current | After |
|---|---|---|---|
| `TelegramCallsUI/VideoChatScreen.swift` | 1833 | `FoundPeer(peer: peer._asPeer(), subscribers: nil)` | `FoundPeer(peer: peer, subscribers: nil)` |
| `ContactListUI/ContactListNode.swift` | 1485 | `FoundPeer(peer: mainPeer._asPeer(), subscribers: nil)` | `FoundPeer(peer: mainPeer, subscribers: nil)` |
| `ContactListUI/ContactListNode.swift` | 1517 | `FoundPeer(peer: $0._asPeer(), subscribers: nil)` (inside `peers.map { … }`) | `FoundPeer(peer: $0, subscribers: nil)` |
| `TelegramBaseController/TelegramBaseController.swift` | 208 | `FoundPeer(peer: peer._asPeer(), subscribers: nil)` | `FoundPeer(peer: peer, subscribers: nil)` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 156 | `FoundPeer(peer: peer._asPeer(), subscribers: nil)` | `FoundPeer(peer: peer, subscribers: nil)` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 265 | `FoundPeer(peer: peer._asPeer(), subscribers: nil)` | `FoundPeer(peer: peer, subscribers: nil)` |
Wrap-needed sites — value at the call site is raw `Peer`, must be wrapped:
| File | Line | Current | After |
|---|---|---|---|
| `ContactListUI/ContactListNode.swift` | 1506 | `mappedPeers.append(FoundPeer(peer: peer.peer, subscribers: subscribers))` | already-EnginePeer (since `peer: FoundPeer` after migration) → `mappedPeers.append(FoundPeer(peer: peer.peer, subscribers: subscribers))`**no edit** |
| `SettingsUI/StorageUsageExceptionsScreen.swift` | 288 | `FoundPeer(peer: peer, subscribers: subscriberCount)` | `FoundPeer(peer: EnginePeer(peer), subscribers: subscriberCount)` |
Note: ContactListNode:1506 is inside a `for peer in mappedPeers` over `[FoundPeer]`, so `peer.peer` is already `EnginePeer` after migration. No edit. Re-classified from C4-wrap-needed to no-op.
So: 4 bridge-drop edits + 1 actual wrap (StorageUsageExceptionsScreen:288) = 5 C4 edits, not 6.
**C3 — drop redundant `EnginePeer(peer.peer)` wrap (22 sites).**
After migration `peer.peer` is already `EnginePeer`, and `EnginePeer.init(_ peer: Peer)` does not accept an EnginePeer argument — so each `EnginePeer(peer.peer)` wrap MUST be dropped to just `peer.peer` or the build fails.
| File | Line | Wraps | Pattern (representative) |
|---|---|---|---|
| `SettingsUI/StorageUsageExceptionsScreen.swift` | 173 | 1 | `EnginePeer(peer.peer).displayTitle(…)``peer.peer.displayTitle(…)` |
| `SettingsUI/StorageUsageExceptionsScreen.swift` | 176 | 1 | `iconPeer: EnginePeer(peer.peer)``iconPeer: peer.peer` |
| `TelegramBaseController/TelegramBaseController.swift` | 265 | 2 | `peer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(…)``peer: peer.peer, title: peer.peer.displayTitle(…)` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 201 | 1 | `peerAvatarCompleteImage(… peer: EnginePeer(peer.peer), …)``peerAvatarCompleteImage(… peer: peer.peer, …)` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 202 | 1 | `text: EnginePeer(peer.peer).displayTitle(…)``text: peer.peer.displayTitle(…)` |
| `PeerInfoScreen/PeerInfoScreenCallActions.swift` | 288 | 2 | `.secondLineWithValue(EnginePeer(peer.peer).displayTitle(…))` and `peerAvatarCompleteImage(… peer: EnginePeer(peer.peer), …)` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1075 | 2 | `peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer))` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1076 | 1 | `interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil, false)` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1078 | 1 | `interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, …)` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 1081 | 1 | `peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location)` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 3088 | 1 | `filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer))` (only the FoundPeer wrap drops; the `EnginePeer(accountPeer)` wrap stays — `accountPeer` is a raw Peer) |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 3096 | 1 | same pattern as 3088 |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 3214 | 1 | same pattern as 3088 |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 3216 | 1 | `entries.append(.localPeer(EnginePeer(peer.peer), …))` |
| `ChatListUI/ChatListSearchListPaneNode.swift` | 3241 | 1 | same pattern as 3088 |
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 171 | 2 | `.secondLineWithValue(EnginePeer(peer.peer).displayTitle(…))` and `peerAvatarCompleteImage(… peer: EnginePeer(peer.peer), …)` |
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 658 | 1 | `peerAvatarCompleteImage(… peer: EnginePeer(peer.peer), …)` |
| `TelegramCallsUI/VideoChatScreenMoreMenu.swift` | 679 | 1 | `text: EnginePeer(peer.peer).displayTitle(…)` |
| **Total** | | **22** | |
Note: only the inner `EnginePeer(peer.peer)` is dropped. Adjacent `EnginePeer(<other>)` wraps (e.g., `EnginePeer(accountPeer)` at lines 3088/3096/3214/3241) are unrelated to this wave and remain.
### Total semantic-edit count
- §1 (TelegramCore): struct (3 lines) + 6 filter rewrites + 4 constructor wraps = ~13 spot edits in one file
- §2 C2: 30 consumer-site downcast rewrites
- §2 C4: 5 consumer-site constructor edits (4 bridge-drops + 1 wrap)
- §2 C3: 22 consumer-site `EnginePeer(peer.peer)` wrap drops
**Total: ~70 semantic edits** across 1 TelegramCore file + 7 consumer files. Type-name mentions in signal/collection signatures need no edit; the type continues to compile.
## Verification
- **Build:** `source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError`
- **Expected outcome:** first-pass-clean build. Errors that surface most likely indicate (a) a missed C2 site, (b) a FoundPeer field-access I missed in the inventory, or (c) a downstream API receiving `peer.peer` that requires raw `Peer` (would need a `._asPeer()` bridge added).
- **Post-build grep validations:**
- `grep -rn "FoundPeer(peer:.*\._asPeer()" submodules/` → expect zero hits in production code (the 4 bridge-drops succeeded).
- `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram" <touched-files>` → expect zero hits in the 7 touched consumer files (FoundPeer-relevant downcasts all rewritten). Other unrelated `something_else.peer.peer as?` patterns may remain on `RenderedPeer` etc.
- `grep -rn "EnginePeer(peer\.peer)" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/"` → expect zero hits in the 7 touched consumer files (other files keep their wraps because their `peer` is non-FoundPeer).
## Risks and mitigations
- **Misnamed enum case bindings (C2).** A wrong binding name (e.g. `if case let .channel(c) = peer.peer` then accessing `channel.info`) compiles but is a typo. *Mitigation:* the rewrites are mechanical and each table-row in §2 above shows the exact target form. Each binding is reused inside the same `if case let` clause.
- **Hidden field accesses missed by the inventory.** *Mitigation:* `--continueOnError` build catches everything in one pass. If 5+ unexpected error sites surface, abandon and re-inventory. If only 12 surface, fix in place.
- **Downstream APIs requiring raw `Peer`.** Some consumer code may pass `foundPeer.peer` to a function taking the `Peer` protocol. Inventory found 2 such sites already simplified (C3), but unknown sites may exist. *Mitigation:* if surfaced by build errors, bridge with `._asPeer()` at the call site (acceptable transitional pattern — these become next-wave candidates for downstream migration).
- **Equatable behavior change.** `Peer.isEqual(_:)` is the protocol's polymorphic identity test; `EnginePeer.==` is the synthesized-or-manual enum equality. *Mitigation:* `EnginePeer.==` is the canonical equality on the enum and is used throughout the engine codebase. The two should agree on identity-relevant fields (peer id, namespace), and FoundPeer equality is used in `Equatable` set/array dedup contexts where both forms produce the same answer for distinct peers. If tests existed, this would be the place to add one — they don't, so we accept the substitution.
## Out-of-scope cleanups (for future waves)
- The downstream `peerAvatarCompleteImage(account:peer:size:)` in `PeerInfoScreenCallActions.swift:202` accepts `EnginePeer` — no change needed there.
- Wave 33's 5th `._asPeer()` bridge (the one not at a `FoundPeer` constructor) remains. It is at a different downstream API — separate wave.
- `SendAsPeer`, `makePeerInfoController`, `makeChatRecentActionsController`, `makeChatQrCodeScreen` migrations — each is its own wave, larger blast radius.

View file

@ -0,0 +1,141 @@
# Wave 35 — `SendAsPeer.peer` `Peer``EnginePeer`
Date: 2026-04-24
Status: approved design, awaiting plan
Wave shape: Peer-typed-API single atomic commit (wave 34 pattern replayed on a smaller target)
## Goal
Eliminate the Postbox-protocol `Peer` leak in the public `SendAsPeer` struct by migrating its `peer` field from `Peer` to `EnginePeer`. Apply wave 34's lessons — comprehensive pre-flight grep including `.peer as?`/`is` casts, outflow-arg patterns, and loop-body `.peer` accesses — to keep post-commit build iterations low.
## Non-goals
- `ContactListPeer.peer(peer: Peer, ...)` case-payload migration — broader blast radius, deferred.
- `canSendMessagesToPeer(_:)` parameter migration — broader blast radius, deferred.
- `makePeerInfoController` / `makeChatQrCodeScreen` / `makeChatRecentActionsController` protocol-method migrations — broader blast radius, deferred.
- `CachedSendAsPeers` cache entry — already `PeerId`-based, entirely inside TelegramCore; no change needed.
- No new engine wrappers, typealiases, or facades introduced in this wave.
## Type change
```swift
// Before
public struct SendAsPeer: Equatable {
public let peer: Peer // Postbox protocol
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) { … }
public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired
}
}
// After
public struct SendAsPeer: Equatable {
public let peer: EnginePeer // TelegramCore value type
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: EnginePeer, subscribers: Int32?, isPremiumRequired: Bool) { … }
// Equatable synthesized — EnginePeer is Equatable.
}
```
## In-scope files
### Category α — TelegramCore (definition + internal construction)
**`submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift`**
- Lines 721: struct definition. Change `peer: Peer``peer: EnginePeer`. Remove manual `==`; rely on synthesized Equatable.
- Line 64 (`_internal_cachedPeerSendAsAvailablePeers`): `SendAsPeer(peer: peer, …)` — wrap raw Postbox `Peer` with `EnginePeer(peer)`.
- Line 170 (`_internal_peerSendAsAvailablePeers`): same wrap.
- Line 236 (`_internal_cachedLiveStorySendAsAvailablePeers`): same wrap.
- Line 330 (`_internal_liveStorySendAsAvailablePeers`): same wrap.
- Lines 87, 90, 259, 262: `peer.peer.id` accesses inside the caching loop — `EnginePeer.id` returns `EnginePeer.Id` which is a typealias for `PeerId`; code keeps compiling.
No other TelegramCore files reference `SendAsPeer`.
### Category β — Pure token/init/access (no body edits expected)
**`submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift`**
- Line 553: `public let sendAsPeers: [SendAsPeer]?` — field typed at collection level, no `.peer` access in this file.
- Lines 751752 / 848 / 1068 / 1408: init parameter, assignment, equality comparison at `[SendAsPeer]?` level, and `updatedSendAsPeers(_:)` method. None reference the inner `.peer` field.
- Expected edits: zero. This file should remain untouched if the field-type migration is clean.
**`submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift`**
- Out of scope: its `openSendAsPeer: (ASDisplayNode, ContextGesture?) -> Void` callback does NOT take a `SendAsPeer`; name-collision only.
### Category γ — Cast-downstream
**`submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift`**
- Lines 20, 26: `peers: [SendAsPeer]` field and constructor — no edit needed.
- Lines 6882: iteration body.
- Line 70: `peer.peer.id.namespace == Namespaces.Peer.CloudUser` — unchanged (EnginePeer.Id retains `.namespace`).
- Line 73: **`if let peer = peer.peer as? TelegramChannel`** → rewrite as `if case let .channel(channelData) = peer.peer`, matching on the `EnginePeer` enum case. Downstream `channelData.info` access behaves the same; `case .broadcast = channelData.info` continues to compile because `EnginePeer.channel` wraps the same `TelegramChannel.Info` enum.
- Lines 89 / 110 / 116 / 121: `EnginePeer(peer.peer)` — drop the wrap, use `peer.peer` directly.
### Category δ — Outflow (construction and field access)
**`submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift`**
- Line 772: `SendAsPeer(peer: peer._asPeer(), …)` — drop `._asPeer()`; construction now takes `EnginePeer` directly. `peer` at this site is already an `EnginePeer` upstream.
- Lines 805, 823: `SendAsPeer(peer: channel, …)` where `channel` is a raw `TelegramChannel` — wrap with `EnginePeer(channel)`.
- Lines 792 / 826 / 835 / 844: `allPeers` array ops and `.peer.id` filter/find — unchanged.
**`submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift`**
- Line 847: `SendAsPeer(peer: sendAsConfiguration.currentPeer._asPeer(), …)` — drop `._asPeer()`. `sendAsConfiguration.currentPeer` is `EnginePeer` upstream.
- Line 851: `updatedSendAsPeers([…])` — unchanged.
**`submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift`**
- Line 1625: `EnginePeer(peer)` where `peer` is now `EnginePeer` → collapses to `peer`.
- Lines 1616 / 1620 / 1622 / 2948 / 5370: `.peer.id` comparisons, `sendAsPeers.first(where:)` — unchanged.
**`submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift`**
- Line 249: `SendAsPeer(peer: accountPeer._asPeer(), …)` — drop `._asPeer()`.
- Line 4080: `(sendAsPeer?.peer).flatMap(EnginePeer.init)` → simplifies to `sendAsPeer?.peer` (already `EnginePeer?`).
- Line 4081: `.map({ EnginePeer($0.peer) })``.map({ $0.peer })`.
- Line 254 / 688 / 701 / 702 / 705 / 4050 / 4068 / 4069 / 4088 / 4089 / 4327 / 4333 / 4340 / 4356 / 4372: `.peer.id` accesses, variable bindings, optional access — unchanged.
- Line 4340: `call.sendStars(fromId: sendAsPeer?.peer.id, …)``EnginePeer.Id == PeerId`, unchanged.
**`submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift`**
- Lines 30563072: `sendMessageContext.currentSendAsPeer` pass-through to context-menu item. Verify call-site type expectations during implementation; likely no edit needed since `ChatSendAsPeerListContextItem` keeps taking `[SendAsPeer]`.
## Out-of-scope — name collisions (do not touch)
- `submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift:271-272``screenState.sendAsPeers` is `[EnginePeer]` (see `ShareWithPeersScreen.swift:1114`). Different type, same name.
- `submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift:1515,2749,2958``availableSendAsPeers: [EnginePeer]` enum-case payload. Different type, same name.
- `submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift:7070`, `ShareWithPeersScreen.swift:39,57,74,817,1301,2352,3284,3453``initialSendAsPeerId: EnginePeer.Id?` / method names containing "SendAsPeer". PeerId parameter, not the struct.
- Callback declarations in `ChatPanelInterfaceInteraction.swift`, `AttachmentPanel.swift`, `PeerSelectionControllerNode.swift`, `ChatRecentActionsController.swift`, `PeerInfoSelectionPanelNode.swift` named `updateShowSendAsPeers` / `openSendAsPeer` — these take `(Bool)`/`(ASDisplayNode, ContextGesture?)`, not `SendAsPeer` values.
## Execution plan outline (for writing-plans)
Single atomic commit ordering:
1. Edit `SendAsPeers.swift` — change field type, init parameter, drop manual `==`, wrap raw `Peer` at the 4 construction sites with `EnginePeer(peer)`.
2. Edit `ChatSendAsPeerListContextItem.swift` — rewrite line 73 cast to EnginePeer case match; drop `EnginePeer(peer.peer)` wraps at 89/110/116/121.
3. Edit `ChatControllerLoadDisplayNode.swift` — drop `._asPeer()` at 772; wrap `channel` with `EnginePeer(channel)` at 805/823.
4. Edit `ChatTextInputPanelComponent.swift` — drop `._asPeer()` at 847.
5. Edit `ChatTextInputPanelNode.swift` — collapse `EnginePeer(peer)` at 1625 to `peer`.
6. Edit `StoryItemSetContainerViewSendMessage.swift` — drop `._asPeer()` at 249; simplify flatMap at 4080; simplify map at 4081.
7. Verify `ChatPresentationInterfaceState.swift` and `StoryItemSetContainerComponent.swift` need no body edits.
8. Build: `source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError`.
9. Fix any files the inventory undercounted (expect scalar `.peer` accesses in closure bodies). Commit once build is green.
## Risk register
| Risk | Mitigation |
|------|------------|
| Inventory undercount (wave 34 lost ~30%) | Pre-flight grep already includes `.peer as?`/`is`/outflow; use `--continueOnError` on first build to surface all sites in one pass. |
| Cast at `ChatSendAsPeerListContextItem:73` doesn't round-trip | `EnginePeer.channel(TelegramChannel)` wraps the exact same concrete type; the `if case let .channel(ch)` rewrite preserves all `ch.info`/`ch.flags`/etc. semantics. |
| `SendAsPeer` Equatable synthesis regression | `EnginePeer` and `Int32?` and `Bool` are all Equatable; synthesized `==` produces the same truth table modulo replacing `Peer.isEqual` with `EnginePeer ==` (which for `.channel(a)` vs `.channel(b)` compares the underlying `TelegramChannel` via its own Equatable). No behavior change expected. |
| `StoryItemSetContainerComponent.swift:3056-3072` outflow missed | Plan step 7 verifies this during implementation; if a wrap/unwrap is needed at the context-menu boundary, add it inline. |
## Validation
- Full Bazel build (`--configuration=debug_sim_arm64 --continueOnError`).
- No TelegramCore/Postbox/TelegramApi errors (scope boundary check — halt if they surface).
- Grep post-commit: `rg "SendAsPeer\(peer: .*\._asPeer" submodules/` returns empty.
- Grep post-commit: `rg "EnginePeer\(.*\.peer\b" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu` returns empty.
## Lessons to carry forward
- Wave 34's grep pattern (`<Type>`-literal token only) undercounted ~30%. This wave's Explore inventory explicitly included `.peer as?`/`is`/outflow-helper/`EnginePeer(.peer)` / `._asPeer()` patterns. Record the post-commit file count vs. pre-commit inventory to calibrate future Peer-typed-API waves.
- Name collisions (different types, same identifier) are a recurring scoping hazard — confirmed in this wave for `sendAsPeers: [EnginePeer]` and `availableSendAsPeers: [EnginePeer]`. Future Peer-typed-API waves should include a name-collision disambiguation pass during inventory.

View file

@ -59,9 +59,9 @@ public enum ContactListAction: Equatable {
}
public enum ContactListPeer: Equatable {
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
case peer(peer: EnginePeer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId {
switch self {
case let .peer(peer, _, _):
@ -70,8 +70,8 @@ public enum ContactListPeer: Equatable {
return .deviceContact(id)
}
}
public var indexName: PeerIndexNameRepresentation {
public var indexName: EnginePeer.IndexName {
switch self {
case let .peer(peer, _, _):
return peer.indexName
@ -79,11 +79,11 @@ public enum ContactListPeer: Equatable {
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
}
}
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer == rhsPeer, lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else {
return false

View file

@ -78,7 +78,8 @@ public func messageMediaFileStatus(context: AccountContext, messageId: MessageId
var thumbnailStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
if let videoThumbnail = file.videoThumbnails.first {
thumbnailStatus = context.account.postbox.mediaBox.resourceStatus(videoThumbnail.resource)
thumbnailStatus = context.engine.resources.status(resource: EngineMediaResource(videoThumbnail.resource))
|> map { $0._asStatus() }
|> map(Optional.init)
}

View file

@ -4,7 +4,6 @@ import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import PresentationDataUtils
import ProgressNavigationButtonNode

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SSignalKit
import SwiftSignalKit

View file

@ -736,9 +736,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.reloadFilters()
}
self.storiesPostingAvailabilityDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
self.storiesPostingAvailabilityDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { view -> AppConfiguration in
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return appConfiguration
}
|> distinctUntilChanged
@ -2698,11 +2698,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if !self.processedFeaturedFilters {
let initializedFeatured = self.context.account.postbox.preferencesView(keys: [
PreferencesKeys.chatListFiltersFeaturedState
])
let initializedFeatured = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.chatListFiltersFeaturedState))
|> mapToSignal { view -> Signal<Bool, NoError> in
if let entry = view.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) {
if let entry = view?.get(ChatListFiltersFeaturedState.self) {
return .single(!entry.filters.isEmpty && !entry.isSeen)
} else {
return .complete()

View file

@ -580,18 +580,18 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
pushControllerImpl?(controller)
})
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
let featuredFilters = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.chatListFiltersFeaturedState))
|> map { preferences -> [ChatListFeaturedFilter] in
guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else {
guard let state = preferences?.get(ChatListFiltersFeaturedState.self) else {
return []
}
return state.filters
}
|> distinctUntilChanged
let updatedFilterOrder = Promise<[Int32]?>(nil)
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.chatListFilterSettings))
let previousDisplayTags = Atomic<Bool?>(value: nil)

View file

@ -437,16 +437,23 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
suggestedPeers = .single([])
}
let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
let accountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> take(1)
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer)
|> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in
if searchQuery?.isEmpty ?? true {
return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))
return .single((peers, dates, selectedFilter?.id, searchQuery, accountPeer))
} else {
return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))))
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, accountPeer)))
}
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in
var suggestedFilters: [ChatListSearchFilter] = []
@ -1056,7 +1063,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if paneKey == .downloads {
let isCachedValue: Signal<Bool, NoError>
if let downloadResource = downloadResource {
isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResource.id), resourceSize: downloadResource.size)
isCachedValue = self.context.engine.resources.status(id: EngineMediaResource.Id(downloadResource.id), resourceSize: downloadResource.size)
|> map { status -> Bool in
switch status {
case .Local:

View file

@ -1015,19 +1015,22 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
var enabled = true
if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer)
enabled = canSendMessagesToPeer(peer.peer._asPeer())
if requiresPremiumForMessaging {
enabled = false
}
}
if filter.contains(.onlyPrivateChats) {
if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) {
switch peer.peer {
case .user, .secretChat:
break
default:
enabled = false
}
}
if filter.contains(.onlyGroups) {
if let _ = peer.peer as? TelegramGroup {
} else if let peer = peer.peer as? TelegramChannel, case .group = peer.info {
if case .legacyGroup = peer.peer {
} else if case let .channel(channel) = peer.peer, case .group = channel.info {
} else {
enabled = false
}
@ -1035,9 +1038,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var suffixString = ""
if let subscribers = peer.subscribers, subscribers != 0 {
if peer.peer is TelegramUser {
if case .user = peer.peer {
suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))"
} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
} else if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
} else {
suffixString = ", \(strings.Conversation_StatusMembers(subscribers))"
@ -1072,13 +1075,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil, false)
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in
interaction.peerSelected(peer.peer, nil, nil, nil, false)
}, disabledAction: { _ in
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
interaction.disabledPeerSelected(peer.peer, nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
}, contextAction: peerContextAction.flatMap { peerContextAction in
return { node, gesture, location in
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location)
peerContextAction(peer.peer, .search(nil), node, gesture, location)
}
}, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in
return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends, stats.hasLiveItems)
@ -1497,14 +1500,14 @@ private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), s
case .channels:
return (
value.0.filter { peer in
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
return true
} else {
return false
}
},
value.1.filter { peer in
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
return true
} else {
return false
@ -2192,7 +2195,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1)
let accountPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { $0._asPeer() }
|> take(1)
let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set<EnginePeer.Id>), NoError>
if case .savedMessagesChats = location {
@ -3076,7 +3088,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
for peer in foundRemotePeers.0 {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
totalNumberOfLocalPeers += 1
}
@ -3084,7 +3096,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var totalNumberOfGlobalPeers = 0
for peer in foundRemotePeers.1 {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) {
totalNumberOfGlobalPeers += 1
}
}
@ -3202,9 +3214,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
break
}
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
entries.append(.localPeer(EnginePeer(peer.peer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false))
entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false))
index += 1
numberOfLocalPeers += 1
}
@ -3229,7 +3241,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
break
}
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery))
@ -3708,7 +3720,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
storyStatsIds.append(foundPeer.peer.id)
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = foundPeer.peer, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id)
}
case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _, _, _):

View file

@ -1884,18 +1884,22 @@ public final class ChatListNode: ListViewImpl {
let savedMessagesPeer: Signal<EnginePeer?, NoError>
if case let .peers(filter, _, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init)
|> map { peer in
return peer.flatMap(EnginePeer.init)
savedMessagesPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map(Optional.init)
} else {
savedMessagesPeer = .single(nil)
}
let hideArchivedFolderByDefault = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatArchiveSettings])
let hideArchivedFolderByDefault = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.chatArchiveSettings))
|> map { view -> Bool in
let settings: ChatArchiveSettings = view.values[ApplicationSpecificPreferencesKeys.chatArchiveSettings]?.get(ChatArchiveSettings.self) ?? .default
let settings: ChatArchiveSettings = view?.get(ChatArchiveSettings.self) ?? .default
return settings.isHiddenByDefault
}
|> distinctUntilChanged

View file

@ -987,7 +987,7 @@ final class ChatSendMessageContextScreenComponent: Component {
loadEffectAnimationSignal = Signal { subscriber in
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start()
let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource)
let dataDisposabke = (context.engine.resources.status(resource: EngineMediaResource(customEffectResource))
|> filter { status in
if status == .Local {
return true

View file

@ -158,13 +158,13 @@ public final class ReactionImageNode: ASDisplayNode {
super.init()
self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
self.disposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource))
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.iconNode.image = image
}

View file

@ -179,11 +179,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
if isGlobal, let _ = peer.addressName {
status = .addressName("")
} else {
if let _ = peer as? TelegramUser {
if case .user = peer {
status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat)
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil)
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if case .group = channel.info {
if let participantCount = participantCount, participantCount != 0 {
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(participantCount)), multiline: false, isActive: false, icon: nil)
@ -201,7 +201,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
status = .none
}
}
itemPeer = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))
itemPeer = .peer(peer: peer, chatPeer: peer)
case let .deviceContact(id, contact):
status = .none
itemPeer = .deviceContact(stableId: id, contact: contact)
@ -249,7 +249,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
interaction.openPeer(peer, .generic, nil, nil)
}, disabledAction: { _ in
if case let .peer(peer, _, _) = peer {
interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
interaction.openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic)
}
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue {
@ -514,7 +514,7 @@ private func contactListNodeEntries(
}
case let .natural(options, _, _):
let sortedPeers = peers.sorted(by: { lhs, rhs in
let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder)
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder)
if result == .orderedSame {
if case let .peer(lhsPeer, _, _) = lhs, case let .peer(rhsPeer, _, _) = rhs {
return lhsPeer.id < rhsPeer.id
@ -629,7 +629,7 @@ private func contactListNodeEntries(
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, nil))
entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, nil))
index += 1
}
@ -687,7 +687,7 @@ private func contactListNodeEntries(
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false, nil))
entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false, nil))
index += 1
}
@ -698,7 +698,7 @@ private func contactListNodeEntries(
if showSelf, let accountPeer {
if let peer = topPeers.first(where: { $0.id == accountPeer.id }) {
let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings)
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle))
entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle))
existingPeerIds.insert(.peer(peer.id))
}
}
@ -744,7 +744,7 @@ private func contactListNodeEntries(
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap {
entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap {
ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends)
}, false, nil))
@ -762,7 +762,7 @@ private func contactListNodeEntries(
let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text("HIDDEN STORIES", AnyHashable(0)), theme: theme, strings: strings)
for item in storySubscriptions.items {
entries.append(.peer(index, .peer(peer: item.peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount, hasUnseenCloseFriends: item.hasUnseenCloseFriends)))
entries.append(.peer(index, .peer(peer: item.peer, isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount, hasUnseenCloseFriends: item.hasUnseenCloseFriends)))
index += 1
}*/
}
@ -841,7 +841,7 @@ private func contactListNodeEntries(
enabled = false
}
if let isPeerEnabled, !isPeerEnabled(EnginePeer(peer)) {
if let isPeerEnabled, !isPeerEnabled(peer) {
enabled = false
}
default:
@ -1362,7 +1362,7 @@ public final class ContactListNode: ASDisplayNode {
state = state.withToggledPeerId(.peer(peer.id))
}
if value {
selectedPeerMap[id] = .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)
selectedPeerMap[id] = .peer(peer: peer, isGlobal: false, participantCount: nil)
} else {
selectedPeerMap.removeValue(forKey: id)
}
@ -1482,7 +1482,7 @@ public final class ContactListNode: ASDisplayNode {
matches = isPeerEnabled(mainPeer)
}
if matches {
resultPeers.append(FoundPeer(peer: mainPeer._asPeer(), subscribers: nil))
resultPeers.append(FoundPeer(peer: mainPeer, subscribers: nil))
}
}
}
@ -1498,7 +1498,7 @@ public final class ContactListNode: ASDisplayNode {
if let maybePresence = presenceMap[peer.peer.id], let presence = maybePresence {
resultPresences[peer.peer.id] = presence
}
if let _ = peer.peer as? TelegramChannel {
if case .channel = peer.peer {
var subscribers: Int32?
if let maybeMemberCount = participantCountMap[peer.peer.id], let memberCount = maybeMemberCount {
subscribers = Int32(memberCount)
@ -1514,7 +1514,7 @@ public final class ContactListNode: ASDisplayNode {
} else {
foundLocalContacts = context.engine.contacts.searchContacts(query: query.lowercased())
|> map { peers, presences -> ([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]) in
return (peers.map({ FoundPeer(peer: $0._asPeer(), subscribers: nil) }), presences)
return (peers.map({ FoundPeer(peer: $0, subscribers: nil) }), presences)
}
}
var foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], []))
@ -1560,18 +1560,18 @@ public final class ContactListNode: ASDisplayNode {
var result = Set<EnginePeer.Id>()
for peer in foundPeers.foundLocalContacts.0 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundPeers.foundRemoteContacts.0 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundPeers.foundRemoteContacts.1 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
@ -1644,7 +1644,7 @@ public final class ContactListNode: ASDisplayNode {
let lowercasedQuery = query.lowercased()
if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
existingPeerIds.insert(accountPeer.id)
peers.append(.peer(peer: accountPeer._asPeer(), isGlobal: false, participantCount: nil))
peers.append(.peer(peer: accountPeer, isGlobal: false, participantCount: nil))
}
}
@ -1655,14 +1655,14 @@ public final class ContactListNode: ASDisplayNode {
existingPeerIds.insert(peer.peer.id)
peers.append(.peer(peer: peer.peer, isGlobal: false, participantCount: peer.subscribers))
if searchDeviceContacts,
let user = peer.peer as? TelegramUser,
case let .user(user) = peer.peer,
let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
}
for peer in remotePeers.0 {
let matches: Bool
if let user = peer.peer as? TelegramUser {
if case let .user(user) = peer.peer {
let phone = user.phone ?? ""
if requirePhoneNumbers && phone.isEmpty {
matches = false
@ -1670,9 +1670,9 @@ public final class ContactListNode: ASDisplayNode {
matches = true
}
} else if searchGroups || searchChannels {
if peer.peer is TelegramGroup && searchGroups {
if case .legacyGroup = peer.peer, searchGroups {
matches = true
} else if let channel = peer.peer as? TelegramChannel {
} else if case let .channel(channel) = peer.peer {
if case .group = channel.info {
matches = searchGroups
} else {
@ -1692,7 +1692,7 @@ public final class ContactListNode: ASDisplayNode {
existingPeerIds.insert(peer.peer.id)
peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers))
if searchDeviceContacts,
let user = peer.peer as? TelegramUser,
case let .user(user) = peer.peer,
let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
@ -1700,7 +1700,7 @@ public final class ContactListNode: ASDisplayNode {
}
for peer in remotePeers.1 {
let matches: Bool
if let user = peer.peer as? TelegramUser {
if case let .user(user) = peer.peer {
let phone = user.phone ?? ""
if requirePhoneNumbers && phone.isEmpty {
matches = false
@ -1708,9 +1708,9 @@ public final class ContactListNode: ASDisplayNode {
matches = true
}
} else if searchGroups || searchChannels {
if peer.peer is TelegramGroup {
if case .legacyGroup = peer.peer {
matches = searchGroups
} else if let channel = peer.peer as? TelegramChannel {
} else if case let .channel(channel) = peer.peer {
if case .group = channel.info {
matches = searchGroups
} else {
@ -1730,7 +1730,7 @@ public final class ContactListNode: ASDisplayNode {
existingPeerIds.insert(peer.peer.id)
peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers))
if searchDeviceContacts,
let user = peer.peer as? TelegramUser,
case let .user(user) = peer.peer,
let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
@ -1939,9 +1939,9 @@ public final class ContactListNode: ASDisplayNode {
context.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: Array(view.2.keys))
}
var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) })
var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false, participantCount: nil) })
for (peer, memberCount) in chatListPeers {
peers.append(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: memberCount))
peers.append(.peer(peer: peer, isGlobal: false, participantCount: memberCount))
}
var existingPeerIds = Set<EnginePeer.Id>()
var disabledPeerIds = Set<EnginePeer.Id>()
@ -1965,7 +1965,7 @@ public final class ContactListNode: ASDisplayNode {
switch contact {
case let .peer(peer, _, _):
if requirePhoneNumbers,
let user = peer as? TelegramUser {
case let .user(user) = peer {
let phone = user.phone ?? ""
if phone.isEmpty {
return false

View file

@ -192,9 +192,9 @@ public class ContactsController: ViewController {
}).strict()
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.authorizationDisposable = (combineLatest(DeviceAccess.authorizationStatus(subject: .contacts), combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]))
self.authorizationDisposable = (combineLatest(DeviceAccess.authorizationStatus(subject: .contacts), combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.contactsSettings)), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]))
|> map { noticeView, preferences, sharedData -> (Bool, ContactsSortOrder) in
let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let settings: ContactsSettings = preferences?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let synchronizeDeviceContacts: Bool = settings.synchronizeContacts
let contactsSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]?.get(ContactSynchronizationSettings.self)
@ -291,7 +291,7 @@ public class ContactsController: ViewController {
scrollToEndIfExists = true
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), purposefulAction: { [weak self] in
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), purposefulAction: { [weak self] in
if fromSearch {
self?.deactivateSearch(animated: false)
self?.switchToChatsController?()
@ -465,10 +465,17 @@ public class ContactsController: ViewController {
let controller = QrCodeScanScreen(context: strongSelf.context, subject: .peer)
controller.showMyCode = { [weak self, weak controller] in
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId)
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in
if let strongSelf = self, let controller = controller {
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil, temporary: false), in: .window(.root))
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer._asPeer(), threadId: nil, temporary: false), in: .window(.root))
}
})
}

View file

@ -161,8 +161,8 @@ private enum ContactListSearchEntry: Comparable, Identifiable {
let peerItem: ContactsPeerItemPeer
switch peer {
case let .peer(peer, _, _):
peerItem = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer))
nativePeer = EnginePeer(peer)
peerItem = .peer(peer: peer, chatPeer: peer)
nativePeer = peer
case let .deviceContact(stableId, contact):
peerItem = .deviceContact(stableId: stableId, contact: contact)
}
@ -178,7 +178,7 @@ private enum ContactListSearchEntry: Comparable, Identifiable {
openPeer(peer, .generic)
}, disabledAction: { _ in
if case let .peer(peer, _, _) = peer {
openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic)
}
}, contextAction: contextAction.flatMap { contextAction in
return nativePeer.flatMap { nativePeer in
@ -404,12 +404,12 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
if let foundRemoteContacts = foundPeers.foundRemoteContacts {
for peer in foundRemoteContacts.0 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundRemoteContacts.1 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
@ -491,7 +491,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
enabled = false
}
}
entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons))
entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer, isGlobal: false, participantCount: nil), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons))
if searchDeviceContacts, case let .user(user) = peer, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
@ -499,14 +499,13 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
}
if let remotePeers = remotePeers {
for peer in remotePeers.0 {
if !(peer.peer is TelegramUser) {
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, categories.contains(.channels) {
} else {
continue
}
if case .user = peer.peer {
} else if case let .channel(channel) = peer.peer, case .broadcast = channel.info, categories.contains(.channels) {
} else {
continue
}
if let user = peer.peer as? TelegramUser {
if case let .user(user) = peer.peer {
if requirePhoneNumbers {
let phone = user.phone ?? ""
if phone.isEmpty {
@ -517,59 +516,58 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
if user.botInfo != nil {
continue
}
}
}
}
if !existingPeerIds.contains(peer.peer.id) {
existingPeerIds.insert(peer.peer.id)
var enabled = true
var requiresPremiumForMessaging = false
if onlyWriteable {
enabled = canSendMessagesToPeer(peer.peer)
enabled = canSendMessagesToPeer(peer.peer._asPeer())
if let value = peerRequiresPremiumForMessaging[peer.peer.id], value {
requiresPremiumForMessaging = true
enabled = false
}
}
entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons))
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
if searchDeviceContacts, case let .user(user) = peer.peer, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
index += 1
}
}
for peer in remotePeers.1 {
if !(peer.peer is TelegramUser) {
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, categories.contains(.channels) {
} else {
continue
}
if case .user = peer.peer {
} else if case let .channel(channel) = peer.peer, case .broadcast = channel.info, categories.contains(.channels) {
} else {
continue
}
if let user = peer.peer as? TelegramUser, requirePhoneNumbers {
if case let .user(user) = peer.peer, requirePhoneNumbers {
let phone = user.phone ?? ""
if phone.isEmpty {
continue
}
}
if !existingPeerIds.contains(peer.peer.id) {
existingPeerIds.insert(peer.peer.id)
var enabled = true
var requiresPremiumForMessaging = false
if onlyWriteable {
enabled = canSendMessagesToPeer(peer.peer)
enabled = canSendMessagesToPeer(peer.peer._asPeer())
if let value = peerRequiresPremiumForMessaging[peer.peer.id], value {
requiresPremiumForMessaging = true
enabled = false
}
}
entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons))
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
if searchDeviceContacts, case let .user(user) = peer.peer, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
}
index += 1

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI

View file

@ -1667,10 +1667,9 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun
hasLegacyAppData = FileManager.default.fileExists(atPath: statusPath)
}
let preferencesSignal: Signal<PreferencesView?, NoError>
let preferencesSignal: Signal<PreferencesEntry?, NoError>
if let context = context {
preferencesSignal = context.account.postbox.preferencesView(keys: [PreferencesKeys.networkSettings])
|> map(Optional.init)
preferencesSignal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.networkSettings))
} else {
preferencesSignal = .single(nil)
}
@ -1693,7 +1692,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun
let experimentalSettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
let networkSettings: NetworkSettings? = preferences?.values[PreferencesKeys.networkSettings]?.get(NetworkSettings.self)
let networkSettings: NetworkSettings? = preferences?.get(NetworkSettings.self)
var leftNavigationButton: ItemListNavigationButton?
if modal {

View file

@ -932,7 +932,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
var isFirstTime = true
self.disposable.set(combineLatest(
messageView,
self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1),
self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> take(1),
translateToLanguage |> take(1)
).start(next: { [weak self] view, preferencesView, translateToLanguage in
let f: () -> Void = {
@ -940,7 +940,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
if let view = view {
strongSelf.peerIsCopyProtected = view.peerIsCopyProtected
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration)
strongSelf.configuration = configuration

View file

@ -93,7 +93,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
private var disposable = MetaDisposable()
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
private var status: EngineMediaResource.FetchStatus?
init(context: AccountContext, presentationData: PresentationData) {
self.context = context
@ -195,7 +195,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
}
private func setupStatus(resource: MediaResource) {
self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(resource)
self.statusDisposable.set((self.context.engine.resources.status(resource: EngineMediaResource(resource))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status

View file

@ -110,7 +110,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
private var status: EngineMediaResource.FetchStatus?
init(context: AccountContext, presentationData: PresentationData) {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
@ -188,7 +188,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
}
private func setupStatus(context: AccountContext, resource: MediaResource) {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource)
self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(resource))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status
@ -238,11 +238,11 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
if let fileName = fileReference.media.fileName {
pathExtension = (fileName as NSString).pathExtension
}
let data = context.account.postbox.mediaBox.resourceData(fileReference.media.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false))
let data = context.engine.resources.data(resource: EngineMediaResource(fileReference.media.resource), pathExtension: pathExtension)
|> deliverOnMainQueue
self.dataDisposable.set(data.start(next: { [weak self] data in
if let strongSelf = self {
if data.complete {
if data.isComplete {
if let webView = strongSelf.webView as? WKWebView {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
let blockRules = """

View file

@ -84,7 +84,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
private var status: EngineMediaResource.FetchStatus?
init(context: AccountContext, presentationData: PresentationData) {
self.containerNode = ASDisplayNode()
@ -183,7 +183,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
}
private func setupStatus(context: AccountContext, resource: MediaResource) {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource)
self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(resource))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status

View file

@ -249,7 +249,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
private let statusDisposable = MetaDisposable()
private let dataDisposable = MetaDisposable()
private let recognitionDisposable = MetaDisposable()
private var status: MediaResourceStatus?
private var status: EngineMediaResource.FetchStatus?
private var fetchedDimensions: PixelDimensions?
private let pagingEnabledPromise = ValuePromise<Bool>(true)
@ -957,7 +957,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
}
private func setupStatus(resource: MediaResource) {
self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(resource)
self.statusDisposable.set((self.context.engine.resources.status(resource: EngineMediaResource(resource))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status

View file

@ -3704,7 +3704,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
allFiles.append(contentsOf: qualitySet.qualityFiles.values)
let qualitySignals = allFiles.map { file -> Signal<(fileId: MediaId, isCached: Bool), NoError> in
return self.context.account.postbox.mediaBox.resourceStatus(file.media.resource)
return self.context.engine.resources.status(resource: EngineMediaResource(file.media.resource))
|> take(1)
|> map { status -> (fileId: MediaId, isCached: Bool) in
return (file.media.fileId, status == .Local)

View file

@ -79,16 +79,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
if case .image = self.stickerPack.type.contentType {
} else {
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
for sticker in strongSelf.stickerPack.stickers {
if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] {
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: EnginePeer(peer), resource: resource, thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType)
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType)
|> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in
switch result {
case .progress:

View file

@ -116,10 +116,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
})
if interactive {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(largest.resource)) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.fetchStatus = status
strongSelf.updateFetchStatus()
}
}

View file

@ -70,10 +70,10 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
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.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.fetchStatus = status
strongSelf.updateFetchStatus()
}
}

View file

@ -624,10 +624,17 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
guard let inviteLink = invite?.link else {
return
}
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true

View file

@ -411,13 +411,20 @@ public final class InviteLinkInviteController: ViewController {
if let invite {
if case let .groupOrChannel(peerId) = self.mode {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -443,10 +450,17 @@ public final class InviteLinkInviteController: ViewController {
return
}
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true

View file

@ -539,10 +539,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -557,10 +564,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -726,10 +740,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
}, action: { _, f in
f(.default)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -796,10 +817,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
}, action: { _, f in
f(.default)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true

View file

@ -741,13 +741,20 @@ public final class InviteLinkViewController: ViewController {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self, let parentController = strongSelf.controller else {
return
}
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -763,10 +770,17 @@ public final class InviteLinkViewController: ViewController {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -823,11 +837,19 @@ public final class InviteLinkViewController: ViewController {
}
if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _, _) = invite {
let creatorPeerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
self.disposable = (combineLatest(
self.presentationDataPromise.get(),
self.importersContext.state,
requestsState,
context.account.postbox.loadedPeerWithId(adminId)
creatorPeerSignal
) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in
if let strongSelf = self {
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
@ -849,7 +871,7 @@ public final class InviteLinkViewController: ViewController {
}
entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, date))
if !requestsState.importers.isEmpty || (state.isLoadingMore && requestsState.count > 0) {
entries.append(.requestHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(Int32(requestsState.count)).uppercased(), "", false))

View file

@ -3,7 +3,6 @@ import UIKit
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import MtProtoKit
import TelegramUIPreferences
import LegacyComponents

View file

@ -265,17 +265,24 @@ public final class LocationViewController: ViewController {
}
strongSelf.controllerNode.setProximityIndicator(radius: 0)
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.subject.id.peerId)
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.subject.id.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
var compactDisplayTitle: String?
if let peer = peer as? TelegramUser {
compactDisplayTitle = EnginePeer(peer).compactDisplayTitle
if case .user = peer {
compactDisplayTitle = peer.compactDisplayTitle
}
let controller = LocationDistancePickerScreen(context: context, style: .default, compactDisplayTitle: compactDisplayTitle, distances: strongSelf.controllerNode.headerNode.mapNode.distancesToAllAnnotations, updated: { [weak self] distance in
guard let strongSelf = self else {
return
@ -370,17 +377,24 @@ public final class LocationViewController: ViewController {
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 30 * 60, proximityNotificationRadius: distance))
})
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.subject.id.peerId)
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.subject.id.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
var compactDisplayTitle: String?
if let peer = peer as? TelegramUser {
compactDisplayTitle = EnginePeer(peer).compactDisplayTitle
if case .user = peer {
compactDisplayTitle = peer.compactDisplayTitle
}
var text: String
let distanceString = shortStringForDistance(strings: strongSelf.presentationData.strings, distance: distance)
if let compactDisplayTitle = compactDisplayTitle {
@ -407,7 +421,14 @@ public final class LocationViewController: ViewController {
)
})
} else {
let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: subject.id.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
var title: String
@ -415,8 +436,8 @@ public final class LocationViewController: ViewController {
title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription
} else {
title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription
if let user = peer as? TelegramUser {
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(EnginePeer(user).compactDisplayTitle).string
if case .user = peer {
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string
}
}

View file

@ -922,15 +922,22 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
return
}
let _ = (self.context.account.postbox.loadedPeerWithId(self.subject.id.peerId)
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.subject.id.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
var text: String = strongSelf.presentationData.strings.Location_ProximityGroupTip
if peer.id.namespace == Namespaces.Peer.CloudUser {
text = strongSelf.presentationData.strings.Location_ProximityTip(EnginePeer(peer).compactDisplayTitle).string
text = strongSelf.presentationData.strings.Location_ProximityTip(peer.compactDisplayTitle).string
}
strongSelf.interaction.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _, _ in

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import FFMpegBinding
import RangeSet

View file

@ -328,12 +328,19 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(mention.peerId)
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: mention.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
guard let strongSelf = self else {
return
}
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
}
})

View file

@ -313,9 +313,16 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat
}
}
}
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { channel in
guard let _ = channel as? TelegramChannel else {
guard case .channel = channel else {
return
}

View file

@ -452,7 +452,14 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
state.revealedPeerId = nil
return state
}
let signal = context.account.postbox.loadedPeerWithId(memberId)
let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: memberId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue
|> mapToSignal { peer -> Signal<Bool, NoError> in
let result = ValuePromise<Bool>()
@ -918,27 +925,27 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
for foundPeer in foundRemotePeers.0 {
let peer = foundPeer.peer
if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = peer, user.botInfo != nil {
continue
}
if !existingPeerIds.contains(peer.id) && peer is TelegramUser {
if !existingPeerIds.contains(peer.id), case .user = peer {
existingPeerIds.insert(peer.id)
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
index += 1
}
}
for foundPeer in foundRemotePeers.1 {
let peer = foundPeer.peer
if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = peer, user.botInfo != nil {
continue
}
if !existingPeerIds.contains(peer.id) && peer is TelegramUser {
if !existingPeerIds.contains(peer.id), case .user = peer {
existingPeerIds.insert(peer.id)
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
index += 1
}
}
@ -1160,28 +1167,28 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
for foundPeer in foundRemotePeers.0 {
let peer = foundPeer.peer
if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = peer, user.botInfo != nil {
continue
}
if !existingPeerIds.contains(peer.id) && peer is TelegramUser {
if !existingPeerIds.contains(peer.id), case .user = peer {
existingPeerIds.insert(peer.id)
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
index += 1
}
}
for foundPeer in foundRemotePeers.1 {
let peer = foundPeer.peer
if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = peer, user.botInfo != nil {
continue
}
if !existingPeerIds.contains(peer.id) && peer is TelegramUser {
if !existingPeerIds.contains(peer.id), case .user = peer {
existingPeerIds.insert(peer.id)
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat))
index += 1
}
}

View file

@ -1051,7 +1051,14 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
}
}
}
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { channel in
dismissController?()
presentControllerImpl?(channelBannedMemberController(context: context, peerId: peerId, memberId: peer.id, initialParticipant: participant?.participant, updated: { _ in

View file

@ -1600,10 +1600,17 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId))
|> deliverOnMainQueue).start(next: { invite in
if let invite = invite {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true
@ -1619,10 +1626,17 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
} else {
isGroup = true

View file

@ -1416,7 +1416,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie
let dataSignal: Signal<(EnginePeer?, DeviceContactStableId?), NoError>
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
guard case let .user(contact) = contact, let phoneNumber = contact.phone else {
return
}
dataSignal = (context.sharedContext.contactDataManager?.basicData() ?? .single([:]))

View file

@ -597,11 +597,11 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|> delay(1.0, queue: Queue.mainQueue())
)
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), chatLocationPromise.get(), displayLoading, expandedPromise.get(), context.account.postbox.preferencesView(keys: [PreferencesKeys.peersNearby]))
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), chatLocationPromise.get(), displayLoading, expandedPromise.get(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.peersNearby)))
|> deliverOnMainQueue
|> map { presentationData, data, chatLocation, displayLoading, expanded, view -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
let state = view.values[PreferencesKeys.peersNearby]?.get(PeersNearbyState.self) ?? .default
let state = view?.get(PeersNearbyState.self) ?? .default
var crossfade = false
if (data?.users.isEmpty ?? true) != (previous?.users.isEmpty ?? true) {

View file

@ -475,8 +475,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
}
return Signal { subscriber in
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start()
let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> filter(\.complete)
let dataDisposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource))
|> filter(\.isComplete)
|> take(1)).start(next: { data in
subscriber.putNext(data.path)
subscriber.putCompletion()

View file

@ -170,10 +170,10 @@ private enum StorageUsageExceptionsEntry: ItemListNodeEntry {
if peer.peer.id == arguments.context.account.peerId {
title = presentationData.strings.DialogList_SavedMessages
} else {
title = EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast)
title = peer.peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast)
}
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer.peer, title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.openPeerMenu(peer.peer.id, value)
}, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id))
}
@ -285,7 +285,7 @@ public func storageUsageExceptionsScreen(
continue
}
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
result.append((peer: FoundPeer(peer: EnginePeer(peer), subscribers: subscriberCount), value: value))
}
return result.sorted(by: { lhs, rhs in

View file

@ -791,10 +791,10 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode {
self?.view.endEditing(true)
}
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.listDisposable = (combineLatest(context.sharedContext.presentationData, statePromise.get(), preferences, context.engine.peers.notificationSoundList()) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, prefs, notificationSoundList in
let entries = notificationsExceptionEntries(presentationData: presentationData, notificationSoundList: notificationSoundList, state: state)
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
@ -1045,10 +1045,10 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont
}
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil)
let stateQuery = stateAndPeers
|> map { stateAndPeers -> String? in
return stateAndPeers.1
@ -1056,7 +1056,7 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont
|> distinctUntilChanged
let searchSignal = stateQuery
|> mapToSignal { query -> Signal<(PresentationData, NotificationSoundList?, (NotificationExceptionState, String?), PreferencesView, [EngineRenderedPeer]), NoError> in
|> mapToSignal { query -> Signal<(PresentationData, NotificationSoundList?, (NotificationExceptionState, String?), PreferencesEntry?, [EngineRenderedPeer]), NoError> in
var contactsSignal: Signal<[EngineRenderedPeer], NoError> = .single([])
if let query = query {
contactsSignal = context.account.postbox.searchPeers(query: query)

View file

@ -779,7 +779,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
})
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
let exceptionsSignal = Signal<NotificationExceptionsList?, NoError>.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init))
@ -858,7 +858,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
|> map { presentationData, sharedData, view, exceptions, authorizationStatus, warningSuppressed, hasMoreThanOneAccount -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
if let settings = view?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings

View file

@ -1007,7 +1007,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
})
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
var automaticData: Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> = .single(([], [:]))
if case .stories = category {
@ -1039,7 +1039,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get(), automaticData)
|> map { presentationData, notificationSoundList, sharedData, view, state, automaticData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
if let settings = view?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings

View file

@ -546,11 +546,11 @@ public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataP
}
|> distinctUntilChanged
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers(), hasBotSettings)
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.contactsSettings)), context.engine.peers.recentPeers(), hasBotSettings)
|> map { presentationData, state, noticeView, sharedData, preferences, recentPeers, hasBotSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) })
let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let settings: ContactsSettings = preferences?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let synchronizeDeviceContacts: Bool = settings.synchronizeContacts

View file

@ -1021,12 +1021,12 @@ public func privacyAndSecurityController(
let privacySignal = privacySettingsPromise.get()
|> take(1)
let callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration]))
let callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration)))
|> take(1)
|> map { sharedData, view -> (VoiceCallSettings, VoipConfiguration) in
let voiceCallSettings: VoiceCallSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) ?? .defaultSettings
let voipConfiguration = view.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let voipConfiguration = view?.get(VoipConfiguration.self) ?? .defaultValue
return (voiceCallSettings, voipConfiguration)
}

View file

@ -811,9 +811,9 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
let previousMode = Atomic<RecentSessionsMode>(value: .sessions)
let enableQRLogin = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
let enableQRLogin = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { view -> Bool in
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
guard let appConfiguration = view?.get(AppConfiguration.self) else {
return false
}
guard let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR else {

View file

@ -384,7 +384,7 @@ public func reactionNotificationSettingsController(
}
)
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
@ -394,7 +394,7 @@ public func reactionNotificationSettingsController(
)
|> map { presentationData, notificationSoundList, preferencesView, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
if let settings = preferencesView?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings

View file

@ -2073,11 +2073,11 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
}
let callsSignal: Signal<(VoiceCallSettings, VoipConfiguration)?, NoError>
if case .voiceCalls = kind {
callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration]))
callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration)))
|> take(1)
|> map { sharedData, view -> (VoiceCallSettings, VoipConfiguration)? in
let voiceCallSettings: VoiceCallSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) ?? .defaultSettings
let voipConfiguration = view.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let voipConfiguration = view?.get(VoipConfiguration.self) ?? .defaultValue
return (voiceCallSettings, voipConfiguration)
}
} else {
@ -4317,11 +4317,11 @@ func settingsSearchableItems(
return accountsAndPeers.1.count + 1 < maximumNumberOfAccounts
}
let notificationSettings = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let notificationSettings = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications))
|> take(1)
|> map { view -> GlobalNotificationSettingsSet in
let viewSettings: GlobalNotificationSettingsSet
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
if let settings = view?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings

View file

@ -888,11 +888,11 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init)))
quickReaction = combineLatest(
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.reactionSettings))
)
|> map { peer, preferencesView -> MessageReaction.Reaction? in
let reactionSettings: ReactionSettings
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
if let entry = preferencesView, let value = entry.get(ReactionSettings.self) {
reactionSettings = value
} else {
reactionSettings = .default

View file

@ -468,10 +468,17 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup
}))
}
}, shareLink: {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let user = peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = peer, user.botInfo != nil {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
} else {
var currentAddressName: String = peer.addressName ?? ""

View file

@ -286,7 +286,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
let foundItems = combineLatest(self.searchQuery.get(), self.themePromise.get())
|> mapToSignal { query, theme -> Signal<([ShareSearchPeerEntry]?, Bool), NoError> in
if !query.isEmpty {
let accountPeer = context.stateManager.postbox.loadedPeerWithId(context.accountPeerId) |> take(1)
let accountPeer = context.engineData.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.accountPeerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { $0._asPeer() }
|> take(1)
let foundLocalPeers = context.stateManager.postbox.searchPeers(query: query.lowercased())
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> = .single(([], [], true))
|> then(
@ -326,12 +335,12 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
}
for peer in foundPeers.foundRemotePeers.0 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundPeers.foundRemotePeers.1 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
@ -390,18 +399,18 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
} else {
for foundPeer in foundRemotePeers.0 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer._asPeer()) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: foundPeer.peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
for foundPeer in foundRemotePeers.1 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer._asPeer()) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
index += 1
}
}

View file

@ -826,7 +826,15 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
let previousData = Atomic<GroupStats?>(value: nil)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(statePromise.get(), presentationData, dataPromise.get(), context.account.postbox.loadedPeerWithId(peerId), peersPromise.get(), longLoadingSignal)
let loadedChannelPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
let signal = combineLatest(statePromise.get(), presentationData, dataPromise.get(), loadedChannelPeer, peersPromise.get(), longLoadingSignal)
|> deliverOnMainQueue
|> map { state, presentationData, data, channelPeer, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
@ -838,9 +846,9 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(accountPeerId: context.account.peerId, state: state, data: data, channelPeer: EnginePeer(channelPeer), peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(accountPeerId: context.account.peerId, state: state, data: data, channelPeer: channelPeer, peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
@ -862,10 +870,17 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
}
openPeerImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController {
let _ = (context.account.postbox.loadedPeerWithId(peer.id)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
})
@ -887,10 +902,17 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
}
openPeerAdminActionsImpl = { [weak controller] participantPeerId in
if let navigationController = controller?.navigationController as? NavigationController {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil)
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: participantPeerId, starsState: nil)
navigationController.pushViewController(controller)
})
}

View file

@ -196,7 +196,14 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
self.view.endEditing(true)
self.context.joinGroupCall(peerId: peerId, invite: invite, requestJoinAsPeerId: { completion in
let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
let currentAccountPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}
@ -233,10 +240,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
var items: [ActionSheetItem] = []
var isGroup = false
for peer in peers {
if peer.peer is TelegramGroup {
if case .legacyGroup = peer.peer {
isGroup = true
break
} else if let peer = peer.peer as? TelegramChannel, case .group = peer.info {
} else if case let .channel(channel) = peer.peer, case .group = channel.info {
isGroup = true
break
}
@ -248,14 +255,14 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
subtitle = presentationData.strings.VoiceChat_PersonalAccount
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
} else {
subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers)
}
}
items.append(VoiceChatPeerActionSheetItem(context: context, peer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: {
items.append(VoiceChatPeerActionSheetItem(context: context, peer: peer.peer, title: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: {
dismissAction()
completion(peer.peer.id)
}))

View file

@ -521,7 +521,6 @@ public final class CallController: ViewController {
confirmation: { peer in
switch peer {
case let .peer(peer, _, _):
let peer = EnginePeer(peer)
guard case let .user(user) = peer else {
return .single(false)
}
@ -539,7 +538,6 @@ public final class CallController: ViewController {
isPeerEnabled: { peer in
switch peer {
case let .peer(peer, _, _):
let peer = EnginePeer(peer)
guard case let .user(user) = peer else {
return false
}

View file

@ -319,15 +319,23 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
switch content {
case let .call(sharedContext, account, call):
self.presentationData = sharedContext.currentPresentationData.with { $0 }
let callPeer = TelegramEngine(account: account).data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
self.stateDisposable.set(
(combineLatest(
account.postbox.loadedPeerWithId(call.peerId),
callPeer,
call.state,
call.isMuted
)
|> deliverOnMainQueue).start(next: { [weak self] peer, state, isMuted in
if let strongSelf = self {
strongSelf.currentPeer = peer
strongSelf.currentPeer = peer._asPeer()
strongSelf.currentCallState = state
strongSelf.currentIsMuted = isMuted

View file

@ -1185,12 +1185,20 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
return
}
let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(peerId), self.callImpl.state |> take(1))
let sharedPeerSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
let _ = (combineLatest(queue: .mainQueue(), sharedPeerSignal, self.callImpl.state |> take(1))
|> deliverOnMainQueue).start(next: { [weak self] peer, callState in
if let strongSelf = self {
var inviteLinks = inviteLinks
if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
if case let .channel(channel) = peer, case .group = channel.info, !channel.flags.contains(.isGigagroup), !(channel.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
let isMuted = defaultParticipantMuteState == .muted
if !isMuted {

View file

@ -320,7 +320,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
if let firstState = ringingStates.first {
if self.currentCall == nil && self.currentGroupCall == nil {
self.currentCallDisposable.set((combineLatest(
firstState.0.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1),
firstState.0.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration),
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)
) |> take(1),
accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1)
)
|> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in
@ -330,12 +333,12 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
if strongSelf.currentUpgradedToConferenceCallId == firstState.2.id {
return
}
let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings
let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let call = PresentationCallImpl(
context: firstState.0,
audioSession: strongSelf.audioSession,
@ -366,7 +369,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
let _ = currentCall.hangUp().startStandalone()
self.currentCallDisposable.set((combineLatest(
firstState.0.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1),
firstState.0.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration),
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)
) |> take(1),
accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1)
)
|> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in
@ -376,11 +382,11 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
if strongSelf.currentUpgradedToConferenceCallId == firstState.2.id {
return
}
let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings
let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let call = PresentationCallImpl(
context: firstState.0,
@ -628,7 +634,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
return peerView.peerIsContact
}
|> take(1),
context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration),
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)
) |> take(1),
accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1),
areVideoCallsAvailable
)
@ -638,10 +647,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
if let currentCall = strongSelf.currentCall {
currentCall.rejectBusy()
}
let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let isVideoPossible: Bool = areVideoCallsAvailable

View file

@ -1182,25 +1182,32 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
if let peerId {
let _ = (self.account.postbox.loadedPeerWithId(peerId)
let _ = (self.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self else {
return
}
var canManageCall = false
if let peer = peer as? TelegramGroup {
if case .creator = peer.role {
if case let .legacyGroup(group) = peer {
if case .creator = group.role {
canManageCall = true
} else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canManageCalls) {
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) {
canManageCall = true
}
} else if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) {
} else if case let .channel(channel) = peer {
if channel.flags.contains(.isCreator) {
canManageCall = true
} else if (peer.adminRights?.rights.contains(.canManageCalls) == true) {
} else if (channel.adminRights?.rights.contains(.canManageCalls) == true) {
canManageCall = true
}
self.peerUpdatesSubscription = self.accountContext.account.viewTracker.polledChannel(peerId: peer.id).start()
self.peerUpdatesSubscription = self.accountContext.account.viewTracker.polledChannel(peerId: channel.id).start()
}
var updatedValue = self.stateValue
updatedValue.canManageCall = canManageCall

View file

@ -677,7 +677,14 @@ final class VideoChatScreenComponent: Component {
return
}
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peerId)
let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
@ -685,7 +692,7 @@ final class VideoChatScreenComponent: Component {
guard let callState = self.callState, let peer = self.peer else {
return
}
let initialTitle = callState.title
let title: String
@ -698,7 +705,7 @@ final class VideoChatScreenComponent: Component {
text = environment.strings.VoiceChat_EditTitleText
}
let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in
let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: chatPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
@ -796,7 +803,14 @@ final class VideoChatScreenComponent: Component {
}
if let peerId = groupCall.peerId {
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peerId)
let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
@ -1807,7 +1821,14 @@ final class VideoChatScreenComponent: Component {
}
})
let currentAccountPeer = groupCall.accountContext.account.postbox.loadedPeerWithId(groupCall.accountContext.account.peerId)
let currentAccountPeer = groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: groupCall.accountContext.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}

View file

@ -292,12 +292,19 @@ extension VideoChatScreenComponent.View {
switch error {
case .privacy:
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peer.id)
let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
})
case .notMutualContact:
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))

View file

@ -168,7 +168,7 @@ extension VideoChatScreenComponent.View {
for peer in displayAsPeers {
if peer.peer.id == callState.myPeerId {
let avatarSize = CGSize(width: 28.0, height: 28.0)
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: peer.peer, size: avatarSize)), action: { [weak self] c, _ in
guard let self else {
return
}
@ -625,10 +625,10 @@ extension VideoChatScreenComponent.View {
var isGroup = false
if let displayAsPeers = self.displayAsPeers {
for peer in displayAsPeers {
if peer.peer is TelegramGroup {
if case .legacyGroup = peer.peer {
isGroup = true
break
} else if let peer = peer.peer as? TelegramChannel, case .group = peer.info {
} else if case let .channel(channel) = peer.peer, case .group = channel.info {
isGroup = true
break
}
@ -645,7 +645,7 @@ extension VideoChatScreenComponent.View {
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
subtitle = environment.strings.VoiceChat_PersonalAccount
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
subtitle = environment.strings.Conversation_StatusSubscribers(subscribers)
} else {
subtitle = environment.strings.Conversation_StatusMembers(subscribers)
@ -655,7 +655,7 @@ extension VideoChatScreenComponent.View {
let isSelected = peer.peer.id == myPeerId
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
let theme = environment.theme
let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)
let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: peer.peer, size: avatarSize)
|> map { image -> UIImage? in
if isSelected, let image = image {
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
@ -676,7 +676,7 @@ extension VideoChatScreenComponent.View {
}
}
items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in
f(.default)
guard let self, case let .group(groupCall) = self.currentCall else {

View file

@ -630,18 +630,25 @@ final class VoiceChatMainStageNode: ASDisplayNode {
self.speakingAudioLevelView = nil
}
self.speakingPeerDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId)
self.speakingPeerDisposable.set((self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: EnginePeer(peer))
strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: peer)
let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:])
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:])
let attributedText = addAttributesToStringWithRanges(presentationData.strings.VoiceChat_ParticipantIsSpeaking(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
let attributedText = addAttributesToStringWithRanges(presentationData.strings.VoiceChat_ParticipantIsSpeaking(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
strongSelf.speakingTitleNode.attributedText = attributedText
strongSelf.speakingContainerNode.alpha = 0.0

View file

@ -2957,7 +2957,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network:
}
}
return peers.map { FoundPeer(peer: $0, subscribers: subscribers[$0.id]) }
return peers.map { FoundPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id]) }
}
}
}
@ -3011,7 +3011,7 @@ func _internal_cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId:
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(FoundPeer(peer: peer, subscribers: subscribers))
peers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers))
}
}
return (peers, cached.timestamp)

View file

@ -5,19 +5,15 @@ import TelegramApi
import MtProtoKit
public struct SendAsPeer: Equatable {
public let peer: Peer
public let peer: EnginePeer
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) {
public init(peer: EnginePeer, subscribers: Int32?, isPremiumRequired: Bool) {
self.peer = peer
self.subscribers = subscribers
self.isPremiumRequired = isPremiumRequired
}
public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired
}
}
public final class CachedSendAsPeers: Codable {
@ -61,7 +57,7 @@ func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId)
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
@ -167,7 +163,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network,
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}
@ -233,7 +229,7 @@ func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: Pee
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
@ -327,7 +323,7 @@ func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}

View file

@ -5,16 +5,16 @@ import TelegramApi
import MtProtoKit
public struct FoundPeer: Equatable {
public let peer: Peer
public let peer: EnginePeer
public let subscribers: Int32?
public init(peer: Peer, subscribers: Int32?) {
public init(peer: EnginePeer, subscribers: Int32?) {
self.peer = peer
self.subscribers = subscribers
}
public static func ==(lhs: FoundPeer, rhs: FoundPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers
return lhs.peer == rhs.peer && lhs.subscribers == rhs.subscribers
}
}
@ -67,9 +67,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
continue
}
if let user = peer as? TelegramUser {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount))
} else {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId]))
}
}
}
@ -82,9 +82,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
continue
}
if let user = peer as? TelegramUser {
renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount))
} else {
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId]))
}
}
}
@ -94,14 +94,14 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
break
case .channels:
renderedMyPeers = renderedMyPeers.filter { item in
if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = item.peer, case .broadcast = channel.info {
return true
} else {
return false
}
}
renderedPeers = renderedPeers.filter { item in
if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = item.peer, case .broadcast = channel.info {
return true
} else {
return false
@ -109,18 +109,18 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
}
case .groups:
renderedMyPeers = renderedMyPeers.filter { item in
if let channel = item.peer as? TelegramChannel, case .group = channel.info {
if case let .channel(channel) = item.peer, case .group = channel.info {
return true
} else if item.peer is TelegramGroup {
} else if case .legacyGroup = item.peer {
return true
} else {
return false
}
}
renderedPeers = renderedPeers.filter { item in
if let channel = item.peer as? TelegramChannel, case .group = channel.info {
if case let .channel(channel) = item.peer, case .group = channel.info {
return true
} else if item.peer is TelegramGroup {
} else if case .legacyGroup = item.peer {
return true
} else {
return false
@ -128,14 +128,14 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
}
case .privateChats:
renderedMyPeers = renderedMyPeers.filter { item in
if item.peer is TelegramUser {
if case .user = item.peer {
return true
} else {
return false
}
}
renderedPeers = renderedPeers.filter { item in
if item.peer is TelegramUser {
if case .user = item.peer {
return true
} else {
return false

View file

@ -441,15 +441,25 @@ public extension TelegramEngine {
|> map { EngineMediaResource.FetchStatus($0) }
}
public func status(
id: EngineMediaResource.Id,
resourceSize: Int64
) -> Signal<EngineMediaResource.FetchStatus, NoError> {
return self.account.postbox.mediaBox.resourceStatus(MediaResourceId(id.stringRepresentation), resourceSize: resourceSize)
|> map { EngineMediaResource.FetchStatus($0) }
}
public func data(
resource: EngineMediaResource,
pathExtension: String?,
waitUntilFetchStatus: Bool
pathExtension: String? = nil,
waitUntilFetchStatus: Bool = false,
attemptSynchronously: Bool = false
) -> Signal<EngineMediaResource.ResourceData, NoError> {
return self.account.postbox.mediaBox.resourceData(
resource._asResource(),
pathExtension: pathExtension,
option: .complete(waitUntilFetchStatus: waitUntilFetchStatus)
option: .complete(waitUntilFetchStatus: waitUntilFetchStatus),
attemptSynchronously: attemptSynchronously
)
|> map { EngineMediaResource.ResourceData($0) }
}

View file

@ -205,12 +205,12 @@ public final class BatchVideoRenderingContext {
).startStrict()
}
if targetContext.dataDisposable == nil {
targetContext.dataDisposable = (self.context.account.postbox.mediaBox.resourceData(targetContext.file.media.resource)
targetContext.dataDisposable = (self.context.engine.resources.data(resource: EngineMediaResource(targetContext.file.media.resource))
|> deliverOnMainQueue).startStrict(next: { [weak self, weak targetContext] data in
guard let self, let targetContext else {
return
}
if data.complete && targetContext.dataPath == nil {
if data.isComplete && targetContext.dataPath == nil {
targetContext.dataPath = data.path
self.update()
}

View file

@ -2404,10 +2404,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let peach = 0x1F351
let coffin = 0x26B0
let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
let appConfiguration = item.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> take(1)
|> map { view in
return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
return view?.get(AppConfiguration.self) ?? .defaultValue
}
let text = item.message.text

View file

@ -431,10 +431,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
guard let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile else {
return .single(nil)
}
return context.account.postbox.mediaBox.resourceData(id: file.resource.id)
return context.engine.resources.data(id: EngineMediaResource.Id(file.resource.id))
|> take(1)
|> mapToSignal { data -> Signal<String?, NoError> in
if !data.complete {
if !data.isComplete {
return .single(nil)
}
return .single(data.path)

View file

@ -1766,7 +1766,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if NativeVideoContent.isHLSVideo(file: file), let minimizedQuality = HLSVideoContent.minimizedHLSQuality(file: .standalone(media: file), codecConfiguration: HLSCodecConfiguration(context: context)) {
let postbox = context.account.postbox
let playlistStatusSignal = postbox.mediaBox.resourceStatus(minimizedQuality.playlist.media.resource)
let playlistStatusSignal = context.engine.resources.status(resource: EngineMediaResource(minimizedQuality.playlist.media.resource))
|> map { status -> MediaResourceStatus in
switch status {
case .Fetching, .Paused:
@ -1796,7 +1796,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
return .single((.Local, nil))
}
return postbox.mediaBox.resourceStatus(preloadData.0.media.resource)
return context.engine.resources.status(resource: EngineMediaResource(preloadData.0.media.resource))
|> map { status -> Bool in
if case .Fetching = status {
return true
@ -1806,7 +1806,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
|> distinctUntilChanged
|> mapToSignal { isFetching -> Signal<(MediaResourceStatus, MediaResourceStatus?), NoError> in
return postbox.mediaBox.resourceRangesStatus(preloadData.0.media.resource)
return context.engine.resources.resourceRangesStatus(resource: EngineMediaResource(preloadData.0.media.resource))
|> map { status -> (MediaResourceStatus, MediaResourceStatus?) in
let preloadRanges = RangeSet(preloadData.1)
let intersection = status.intersection(preloadRanges)

View file

@ -357,8 +357,15 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
return .single(peer?._asPeer())
}
} else {
resolveSignal = context.account.postbox.loadedPeerWithId(strongSelf.peer.id)
|> map(Optional.init)
resolveSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peer.id))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { Optional($0._asPeer()) }
}
strongSelf.resolvePeerByNameDisposable.set((resolveSignal
|> deliverOnMainQueue).startStrict(next: { peer in

View file

@ -70,8 +70,8 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
subtitle = presentationData.strings.VoiceChat_PersonalAccount
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel {
if case .broadcast = peer.info {
if case let .channel(channel) = peer.peer {
if case .broadcast = channel.info {
subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers)
} else {
subtitle = presentationData.strings.VoiceChat_DiscussionGroup
@ -86,7 +86,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen
selectedItemIndex = i
}
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
let avatarSignal = peerAvatarCompleteImage(account: item.context.account, peer: EnginePeer(peer.peer), size: avatarSize)
let avatarSignal = peerAvatarCompleteImage(account: item.context.account, peer: peer.peer, size: avatarSize)
|> map { image -> UIImage? in
if isSelected, let image = image {
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
@ -107,18 +107,18 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen
}
}
let action = ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), textIcon: { theme in
let action = ContextMenuActionItem(text: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), textIcon: { theme in
return !item.isPremium && peer.isPremiumRequired ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.badgeInactiveFillColor) : nil
}, action: { _, f in
f(.default)
if !item.isPremium && peer.isPremiumRequired {
item.presentToast(EnginePeer(peer.peer))
item.presentToast(peer.peer)
return
}
if peer.peer.id != item.selectedPeerId {
item.action(EnginePeer(peer.peer))
item.action(peer.peer)
}
})
let actionNode = ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: {}, requestUpdateAction: { _, _ in

View file

@ -86,7 +86,7 @@ public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessage
for peer in self.contactPeers {
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
guard case let .user(contact) = contact, let phoneNumber = contact.phone else {
continue
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")

View file

@ -845,7 +845,7 @@ public final class ChatTextInputPanelComponent: Component {
if let sendAsConfiguration = component.sendAsConfiguration {
presentationInterfaceState = presentationInterfaceState.updatedSendAsPeers([SendAsPeer(
peer: sendAsConfiguration.currentPeer._asPeer(),
peer: sendAsConfiguration.currentPeer,
subscribers: sendAsConfiguration.subscriberCount.flatMap(Int32.init(clamping:)),
isPremiumRequired: sendAsConfiguration.isPremiumLocked
)]).updatedShowSendAsPeers(sendAsConfiguration.isSelecting).updatedCurrentSendAsPeerId(sendAsConfiguration.currentPeer.id)

View file

@ -1622,7 +1622,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
currentPeer = sendAsPeers.first?.peer
}
if let context = self.context, let peer = currentPeer {
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: peer, emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
}
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil {
hasMenuButton = true

View file

@ -140,9 +140,9 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode {
self.context = context
self.emoji = emoji
self.configuration.set(self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
self.configuration.set(self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { preferencesView -> InteractiveEmojiConfiguration? in
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
return InteractiveEmojiConfiguration.with(appConfiguration: appConfiguration)
})
self.emojis.set(context.engine.stickers.loadedStickerPack(reference: .dice(emoji), forceActualized: false)

View file

@ -1,9 +1,7 @@
import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
import TelegramUIPreferences
@ -18,6 +16,7 @@ import TextFormat
import WallpaperBackgroundNode
import AnimationCache
import MultiAnimationRenderer
import Postbox
public struct ChatInterfaceHighlightedState: Equatable {
public struct Quote: Equatable {

View file

@ -38,8 +38,8 @@ private func randomGenericReactionEffect(context: AccountContext) -> Signal<Stri
}
return Signal { subscriber in
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start()
let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> filter(\.complete)
let dataDisposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource))
|> filter(\.isComplete)
|> take(1)).start(next: { data in
subscriber.putNext(data.path)
subscriber.putCompletion()
@ -1207,10 +1207,10 @@ public final class EmojiStatusSelectionController: ViewController {
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation?._parse() {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
return context.engine.resources.data(resource: EngineMediaResource(aroundAnimation.resource))
|> take(1)
|> map { data -> String? in
if data.complete {
if data.isComplete {
return data.path
} else {
return nil
@ -1396,10 +1396,10 @@ public final class EmojiStatusSelectionController: ViewController {
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation?._parse() {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
return context.engine.resources.data(resource: EngineMediaResource(aroundAnimation.resource))
|> take(1)
|> map { data -> String? in
if data.complete {
if data.isComplete {
return data.path
} else {
return nil

View file

@ -83,8 +83,8 @@ public func ageVerificationAvailability(context: AccountContext) -> Signal<AgeVe
let fetchStatus = Signal<FetchStatus, NoError> { subscriber in
let fetchedDisposable = fetchedData.start()
let resourceDataDisposable = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: false).start(next: { next in
if next.complete {
let resourceDataDisposable = context.engine.resources.data(resource: EngineMediaResource(file.resource)).start(next: { next in
if next.isComplete {
SSZipArchive.unzipFile(atPath: next.path, toDestination: NSTemporaryDirectory())
subscriber.putNext(.completed(compiledModelPath))
subscriber.putCompletion()

View file

@ -316,9 +316,9 @@ public final class GlobalControlPanelsContext {
if chatListNotices {
let twoStepData: Signal<TwoStepVerificationConfiguration?, NoError> = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init))
let accountFreezeConfiguration = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
let accountFreezeConfiguration = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { view -> AppConfiguration in
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return appConfiguration
}
|> distinctUntilChanged

View file

@ -75,8 +75,8 @@ public func cutoutAvailability(context: AccountContext) -> Signal<CutoutAvailabi
let fetchStatus = Signal<FetchStatus, NoError> { subscriber in
let fetchedDisposable = fetchedData.start()
let resourceDataDisposable = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: false).start(next: { next in
if next.complete {
let resourceDataDisposable = context.engine.resources.data(resource: EngineMediaResource(file.resource)).start(next: { next in
if next.isComplete {
SSZipArchive.unzipFile(atPath: next.path, toDestination: NSTemporaryDirectory())
subscriber.putNext(.completed(compiledModelPath))
subscriber.putCompletion()

View file

@ -82,9 +82,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha
signal = .single({ _ in return .stickers([]) })
}
let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
let stickerConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { preferencesView -> StickersSearchConfiguration in
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
return StickersSearchConfiguration.with(appConfiguration: appConfiguration)
}
let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in

View file

@ -957,7 +957,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
combineLatest(notificationExceptions, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()),
combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks),
hasPassport,
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)),
context.engine.notices.getServerProvidedSuggestions(),
context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
@ -988,7 +988,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
})
var enableQRLogin = false
let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self)
let appConfiguration = accountPreferences?.get(AppConfiguration.self)
if let appConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR {
enableQRLogin = true
}
@ -2116,7 +2116,7 @@ func peerInfoScreenData(
requestsStatePromise.get(),
hasStories,
threadData,
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)),
accountIsPremium,
hasSavedMessages,
hasSavedMessagesChats,
@ -2196,7 +2196,7 @@ func peerInfoScreenData(
let peerNotificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings
let threadNotificationSettings = threadData?.notificationSettings
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
return .single(PeerInfoScreenData(
peer: peerView.peers[groupId],

View file

@ -3702,7 +3702,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
func performBotCommand(command: PeerInfoBotCommand) {
let _ = (self.context.account.postbox.loadedPeerWithId(peerId)
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let strongSelf = self else {
return

View file

@ -144,7 +144,14 @@ extension PeerInfoScreenNode {
func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) {
let dismissOnSelection = contextController == nil
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
let currentAccountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}
@ -165,10 +172,10 @@ extension PeerInfoScreenNode {
var isGroup = false
for peer in peers {
if peer.peer is TelegramGroup {
if case .legacyGroup = peer.peer {
isGroup = true
break
} else if let peer = peer.peer as? TelegramChannel, case .group = peer.info {
} else if case let .channel(channel) = peer.peer, case .group = channel.info {
isGroup = true
break
}
@ -183,7 +190,7 @@ extension PeerInfoScreenNode {
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(channel) = peer.peer, case .broadcast = channel.info {
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
} else {
subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers)
@ -191,8 +198,8 @@ extension PeerInfoScreenNode {
}
let avatarSize = CGSize(width: 28.0, height: 28.0)
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize)
items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
if dismissOnSelection {
f(.dismissWithoutContent)
}
@ -246,7 +253,14 @@ extension PeerInfoScreenNode {
let context = self.context
let peerId = self.peerId
let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
let currentAccountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}
@ -271,7 +285,7 @@ extension PeerInfoScreenNode {
}
if let peer = selectedPeer {
let avatarSize = CGSize(width: 28.0, height: 28.0)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { c, f in
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in
guard let strongSelf = self else {
return
}

View file

@ -171,8 +171,15 @@ extension PeerInfoScreenNode {
return .single(peer?._asPeer())
}
} else {
resolveSignal = self.context.account.postbox.loadedPeerWithId(self.peerId)
|> map(Optional.init)
resolveSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> map { Optional($0._asPeer()) }
}
var cancelImpl: (() -> Void)?
let presentationData = self.presentationData

View file

@ -60,13 +60,20 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa
contactsController.navigationPresentation = .modal
confirmationImpl = { [weak contactsController] peerId in
return context.account.postbox.loadedPeerWithId(peerId)
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer, NoError> in
if let peer {
return .single(peer)
} else {
return .never()
}
}
|> deliverOnMainQueue
|> mapToSignal { peer in
let result = ValuePromise<Bool>()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let contactsController = contactsController {
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, actions: [
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {
result.set(false)
}),

View file

@ -899,8 +899,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var selectedPeerMap: [EnginePeer.Id: EnginePeer] = [:]
for contactPeer in selectedContactPeers {
if case let .peer(peer, _, _) = contactPeer {
selectedPeers.append(EnginePeer(peer))
selectedPeerMap[peer.id] = EnginePeer(peer)
selectedPeers.append(peer)
selectedPeerMap[peer.id] = peer
}
}
return (selectedPeers, selectedPeerMap)
@ -1589,7 +1589,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
contactListNode.openPeer = { [weak self] peer, _, _, _ in
if case let .peer(peer, _, _) = peer {
self?.contactListNode?.listNode.clearHighlightAnimated(true)
self?.requestOpenPeer?(EnginePeer(peer), nil)
self?.requestOpenPeer?(peer, nil)
}
}
contactListNode.openDisabledPeer = { [weak self] peer, reason in

View file

@ -245,10 +245,10 @@ public func quickReactionSetupController(
}
)
let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
let settings = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.reactionSettings))
|> map { preferencesView -> ReactionSettings in
let reactionSettings: ReactionSettings
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
if let entry = preferencesView, let value = entry.get(ReactionSettings.self) {
reactionSettings = value
} else {
reactionSettings = .default

View file

@ -927,7 +927,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
signal = chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .media(media: .standalone(media: tmpImage), resource: imageResource))
statusSignal = context.account.postbox.mediaBox.resourceStatus(imageResource)
statusSignal = context.engine.resources.status(resource: EngineMediaResource(imageResource))
|> map { $0._asStatus() }
} else {
displaySize = CGSize(width: 1.0, height: 1.0)
contentSize = displaySize

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