Files
Telegram-iOS/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift
T
isaacisaacClaude Opus 4.7
86d1456552 Postbox -> TelegramEngine waves 107-137 (squashed)
31 waves of consumer-side migration from `import Postbox` to TelegramEngine
typealiases. Net: 173 import drops + 39 BUILD-dep drops + 1 new typealias
(`EngineStoryId = StoryId`, wave 113).

Wave shapes used:
- Orphan-import sweeps (107, 108, 128): drop `import Postbox` from files
  whose only Postbox-symbol reference was the import line itself, then
  resolve build failures. Methodology requires token-level (`grep -oE`)
  filtering, not line-level, to avoid masking real Postbox usage on lines
  that also contain `Namespaces.X` references.
- Identifier-swap mini-waves (109-127, 129-134, 136-137): rename
  Postbox-typealiased identifiers to engine equivalents
  (PeerId -> EnginePeer.Id, MessageId -> EngineMessage.Id,
  MediaId -> EngineMedia.Id, MessageIndex -> EngineMessage.Index,
  StoryId -> EngineStoryId, ItemCollectionId -> EngineItemCollectionId,
  PreferencesEntry -> EnginePreferencesEntry,
  FetchResourceSourceType/Error -> EngineFetchResourceSourceType/Error,
  MemoryBuffer -> EngineMemoryBuffer, MessageTags -> EngineMessage.Tags,
  MessageAttribute -> EngineMessage.Attribute,
  TempBox -> EngineTempBox).
- Asset-string FP-only orphans (124).
- Typealias addition + drain (113): added `EngineStoryId` typealias to
  TelegramCore, then drained 3+11 consumer sites.

Hard blockers identified during these waves (must restore `import Postbox`
when present): MediaResource[A-Za-z]* (any suffix -- the literal
`MediaResource` matches don't catch MediaResourceData/MediaResourceId/etc.),
Postbox/MediaBox/MediaResource raw types, PostboxCoding/PostboxEncoder/
PostboxDecoder, TempBoxFile, ValueBoxKey, PostboxView, combinedView,
HashFunctions, postboxLog, openPostbox, declareEncodable, PeerView,
MessageHistoryView, MessageHistoryThreadData, CachedPeerData, RenderedPeer,
SelectivePrivacyPeer, SimpleDictionary, ItemCollectionInfosView,
ItemCollectionItem, ItemCollectionItemIndex, ItemCollectionViewEntryIndex,
ChatListIndex, ChatListEntrySummaryComponents, CodableEntry,
MessageHistoryThread, MessageHistoryAnchorIndex,
MessageHistoryEntryLocation, PeerStoryStats, PeerNameIndex,
PeerSummaryCounterTags, ChatListTotalUnreadStateCategory/Stats,
arePeersEqual. Protocol-shape blockers: bare `Peer`/`Message`/`Media`
in function signatures, generic args, enum-case payloads, or dict value
types (e.g., `[PeerId: Peer]`, `case messages([Message])`,
`Signal<(Peer?, ...), NoError>`).

`replace_all PeerId -> EnginePeer.Id` is dangerous: mangles compound
names like `failedPeerId`, `ContactListPeerId`, `nextRemoteMediaId`,
`replyToMessageId`. Pre-flight grep `\b[a-z][a-zA-Z]*PeerId\b` and only
replace_all if 0 matches.

Also removes unneeded design/plan docs from a separate (link-highlighting)
feature branch:
- docs/superpowers/plans/2026-05-02-link-highlighting-modern-path-fixes.md
- docs/superpowers/specs/2026-05-02-link-highlighting-modern-path-fixes-design.md

Squashed commits: 6d82c2980d..e6de5d53a3 (59 commits, including
per-wave content commits and per-wave CLAUDE.md bumps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:28:50 +02:00

140 lines
7.2 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import TelegramCore
import AsyncDisplayKit
import Display
import ContextUI
import UndoUI
import AccountContext
import ChatControllerInteraction
import AnimatedTextComponent
import ChatMessagePaymentAlertController
import TelegramPresentationData
import TelegramNotices
extension ChatControllerImpl {
func presentPaidMessageAlertIfNeeded(count: Int32 = 1, forceDark: Bool = false, alwaysAsk: Bool = false, completion: @escaping (Bool) -> Void) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init) else {
completion(false)
return
}
guard let renderedPeer = self.presentationInterfaceState.renderedPeer.flatMap(EngineRenderedPeer.init) else {
return
}
if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars, self.presentationInterfaceState.interfaceState.editMessage == nil {
let totalAmount = sendPaidMessageStars.value * Int64(count)
let _ = (ApplicationSpecificNotice.dismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] dismissedAmount in
guard let self, let starsContext = self.context.starsContext else {
return
}
if !alwaysAsk, let dismissedAmount, dismissedAmount == sendPaidMessageStars.value, let currentState = starsContext.currentState, currentState.balance.value > totalAmount {
if count < 3 && totalAmount < 100 {
completion(false)
} else {
completion(true)
self.displayPaidMessageUndo(count: count, amount: sendPaidMessageStars)
}
} else {
var presentationData = self.presentationData
if forceDark {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
var peer = peer
var renderedPeer = renderedPeer
if let peerDiscussionId = self.presentationInterfaceState.peerDiscussionId, let channel = self.contentData?.state.peerView?.peers[peerDiscussionId] {
peer = EnginePeer(channel)
renderedPeer = EngineRenderedPeer(peer: peer)
}
let controller = chatMessagePaymentAlertController(
context: self.context,
presentationData: presentationData,
updatedPresentationData: nil,
peers: [renderedPeer],
count: count,
amount: sendPaidMessageStars,
totalAmount: nil,
hasCheck: !alwaysAsk,
navigationController: self.navigationController as? NavigationController,
completion: { [weak self] dontAskAgain in
guard let self else {
return
}
if dontAskAgain {
let _ = ApplicationSpecificNotice.setDismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id, amount: sendPaidMessageStars.value).start()
}
if let currentState = starsContext.currentState, currentState.balance.value < totalAmount {
let _ = (self.context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self else {
return
}
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), targetPeerId: nil, customTheme: nil, completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
completion(false)
})
})
self.push(controller)
})
} else {
completion(false)
}
}
)
self.present(controller, in: .window(.root))
}
})
} else {
completion(false)
}
}
func displayPaidMessageUndo(count: Int32, amount: StarsAmount) {
guard let peerId = self.chatLocation.peerId else {
return
}
if let current = self.currentPaidMessageUndoController {
self.currentPaidMessageUndoController = nil
current.dismiss()
self.context.engine.messages.forceSendPostponedPaidMessage(peerId: peerId)
}
let title = self.presentationData.strings.Chat_PaidMessage_Sent_Title(count)
let text = self.presentationData.strings.Chat_PaidMessage_Sent_Text(self.presentationData.strings.Chat_PaidMessage_Sent_Text_Stars(Int32(clamping: amount.value * Int64(count)))).string
let textItems: [AnimatedTextComponent.Item] = [
AnimatedTextComponent.Item(id: 0, content: .text(text))
]
let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak self] action in
guard let self else {
return false
}
if case .undo = action {
var messageIds: [EngineMessage.Id] = []
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemNodeProtocol {
for message in itemNode.messages() {
if message.id.namespace == Namespaces.Message.Local {
messageIds.append(message.id)
}
}
}
}
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).startStandalone()
}
return false
})
self.currentPaidMessageUndoController = controller
self.present(controller, in: .current)
}
}