mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
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:
parent
9187fbb6db
commit
8408e0ae19
130 changed files with 4696 additions and 524 deletions
|
|
@ -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 1–26. 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` |
|
||||
|
|
|
|||
|
|
@ -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 200–210 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 248–256 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 840–848 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 176–200 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 1964–1976 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 515–522 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 285–300 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 160–185 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 315–320 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 488–495 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 510–535 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: 0–3 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 1–3 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.
|
||||
1287
docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md
Normal file
1287
docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 70–90 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 1–2 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 800–812 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 818–830 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 843–853 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 1615–1630 to see the full context. `peer` is bound from a preceding `var currentPeer = sendAsPeers.first(where: { $0.peer.id == ... })?.peer` (lines 1620–1622). 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 1623–1628 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 244–254 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 4078–4082 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 4079–4083 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 3056–3072 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 2–6. 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: 0–5 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 1–3 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.
|
||||
|
|
@ -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 4–6 facade waves rather than 2–3.
|
||||
|
||||
**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, ~50–60s 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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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 96–109, `.groups` lines 110–128, `.privateChats` lines 129–143) 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 1–2 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.
|
||||
|
|
@ -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 7–21: 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 751–752 / 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 68–82: 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 3056–3072: `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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import Display
|
|||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ProgressNavigationButtonNode
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Foundation
|
|||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SSignalKit
|
||||
import SwiftSignalKit
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Foundation
|
|||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import UIKit
|
|||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import MtProtoKit
|
||||
import TelegramUIPreferences
|
||||
import LegacyComponents
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import FFMpegBinding
|
||||
import RangeSet
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([:]))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ?? ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: "")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue