Files
Telegram-iOS/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift
T
isaacisaacClaude Opus 4.7
69bfc65da7 Postbox refactor waves 138–276: 168-commit squash
Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump,
typealias additions, AnyObject→EngineMedia restoration) into one squashed
commit. Migrates dozens of consumer-side public APIs, struct fields,
protocol methods, and enum payloads from Postbox protocols/structs to
TelegramEngine engine wrappers and typealiases. Drops `import Postbox`
from many files. Adds new TelegramCore typealiases and one
TelegramEngineUnauthorized facade.

Notable changes by category:

**TelegramCore typealias additions** (rule 2 — narrow utility typealiases):
- EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex,
  EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters,
  EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex,
  EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats,
  EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState,
  EngineItemCacheEntryId, EngineHashFunctions,
  EngineCachedMediaResourceRepresentationResult,
  EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError,
  EngineMediaResourceStatus, EngineCachedPeerData

**TelegramCore engine extensions/forwarders**:
- EngineMessage.engineMedia, EngineMessage.enginePeers,
  EngineMessage.adAttribute, EngineMessage.effectivelyIncoming
- engineFileSize forwarder
- TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>)
- TelegramEngine.Resources.fetchStatus(id:resourceSize:)
- TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData

**Public API/struct migrations to engine types**:
- ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer]
- WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer]
- CacheUsageStats.peers → [EnginePeer.Id: EnginePeer]
- PeerCommand.peer → EnginePeer
- PeerInfoControllerMode.calls(messages:) → [EngineMessage]
- CallControllerNodeProtocol.updatePeer → EnginePeer params
- ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage?
- ChatHistorySearchContainerNode.messageForGallery → EngineMessage?
- PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible /
  transitionNodeForGallery → engine-typed
- GalleryHiddenMediaTarget.getTransitionInfo /
  GalleryHiddenMediaManager.findTarget → engine-typed
- ChatPanelInterfaceInteraction.presentReactionDeletionOptions /
  presentBan*MessageOptions → EnginePeer
- DrawingMessageRenderer.messages → [EngineMessage]
- ChatVideoGalleryItemScrubberView.setFetchStatusSignal →
  EngineMediaResource.FetchStatus
- ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer,
  VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer,
  MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer,
  ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers,
  ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot,
  PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars,
  ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types

**Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper):
- LegacyCallControllerNode.peer
- CallStatusBarNode.currentPeer

**Dead-code / dead-field removals**:
- CallController.peer, CallControllerNodeV2.account,
  ContactMultiselectionController PeerNameIndex fields,
  preparedChatListNodeViewTransition account: Account param,
  FetchResource.swift entirely (unused function)

**Module-level Postbox import drops**: 30+ files including TelegramRootController,
EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource,
PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers,
PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases,
DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager,
RecentSessionsController, GifContext, AuthorizationSequenceController,
PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode,
PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files.

**AnyObject restoration**: rule 8 added (never substitute Postbox protocols
with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143
back to EngineMedia.

Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders.

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

396 lines
15 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ReactionImageComponent
import WebPBinding
import EmojiStatusSelectionComponent
import EntityKeyboard
private final class QuickReactionSetupControllerArguments {
let context: AccountContext
let openQuickReaction: () -> Void
let toggleReaction: () -> Void
init(
context: AccountContext,
openQuickReaction: @escaping () -> Void,
toggleReaction: @escaping () -> Void
) {
self.context = context
self.openQuickReaction = openQuickReaction
self.toggleReaction = toggleReaction
}
}
private enum QuickReactionSetupControllerSection: Int32 {
case demo
case items
}
public enum QuickReactionSetupEntryTag: ItemListItemTag, Equatable {
case choose
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? QuickReactionSetupEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
enum StableId: Hashable {
case demoHeader
case demoMessage
case demoDescription
case quickReaction
case quickReactionDescription
}
case demoHeader(String)
case demoMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction?, accountPeer: EnginePeer?)
case demoDescription(String)
case quickReaction(String, MessageReaction.Reaction, AvailableReactions)
case quickReactionDescription(String)
var section: ItemListSectionId {
switch self {
case .demoHeader, .demoMessage, .demoDescription:
return QuickReactionSetupControllerSection.demo.rawValue
case .quickReaction, .quickReactionDescription:
return QuickReactionSetupControllerSection.items.rawValue
}
}
var stableId: StableId {
switch self {
case .demoHeader:
return .demoHeader
case .demoMessage:
return .demoMessage
case .demoDescription:
return .demoDescription
case .quickReaction:
return .quickReaction
case .quickReactionDescription:
return .quickReactionDescription
}
}
var sortId: Int {
switch self {
case .demoHeader:
return 0
case .demoMessage:
return 1
case .demoDescription:
return 2
case .quickReaction:
return 3
case .quickReactionDescription:
return 4
}
}
static func ==(lhs: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool {
switch lhs {
case let .demoHeader(text):
if case .demoHeader(text) = rhs {
return true
} else {
return false
}
case let .demoMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsAvailableReactions, lhsReaction, lhsAccountPeer):
if case let .demoMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsAvailableReactions, rhsReaction, rhsAccountPeer) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsAvailableReactions == rhsAvailableReactions, lhsReaction == rhsReaction, lhsAccountPeer?.id == rhsAccountPeer?.id {
return true
} else {
return false
}
case let .demoDescription(text):
if case .demoDescription(text) = rhs {
return true
} else {
return false
}
case let .quickReaction(lhsText, lhsReaction, lhsAvailableReactions):
if case let .quickReaction(rhsText, rhsReaction, rhsAvailableReactions) = rhs, lhsText == rhsText, lhsReaction == rhsReaction, lhsAvailableReactions == rhsAvailableReactions {
return true
} else {
return false
}
case let .quickReactionDescription(text):
if case .quickReactionDescription(text) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! QuickReactionSetupControllerArguments
switch self {
case let .demoHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .demoMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, availableReactions, reaction, accountPeer):
return ReactionChatPreviewItem(
context: arguments.context,
theme: presentationData.theme,
strings: presentationData.strings,
systemStyle: .glass,
sectionId: self.section,
fontSize: fontSize,
chatBubbleCorners: chatBubbleCorners,
wallpaper: wallpaper,
dateTimeFormat: dateTimeFormat,
nameDisplayOrder: nameDisplayOrder,
availableReactions: availableReactions,
reaction: reaction,
accountPeer: accountPeer?._asPeer(),
toggleReaction: {
arguments.toggleReaction()
}
)
case let .demoDescription(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .quickReaction(title, reaction, availableReactions):
return ItemListReactionItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, reaction: reaction, availableReactions: availableReactions, sectionId: self.section, style: .blocks, action: {
arguments.openQuickReaction()
})
case let .quickReactionDescription(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct QuickReactionSetupControllerState: Equatable {
var hasReaction: Bool = false
}
private func quickReactionSetupControllerEntries(
presentationData: PresentationData,
availableReactions: AvailableReactions?,
reactionSettings: ReactionSettings,
state: QuickReactionSetupControllerState,
isPremium: Bool,
accountPeer: EnginePeer?
) -> [QuickReactionSetupControllerEntry] {
var entries: [QuickReactionSetupControllerEntry] = []
if let availableReactions = availableReactions {
entries.append(.demoHeader(presentationData.strings.Settings_QuickReactionSetup_DemoHeader))
entries.append(.demoMessage(
wallpaper: presentationData.chatWallpaper,
fontSize: presentationData.chatFontSize,
bubbleCorners: presentationData.chatBubbleCorners,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
availableReactions: availableReactions,
reaction: state.hasReaction ? reactionSettings.effectiveQuickReaction(hasPremium: isPremium) : nil,
accountPeer: accountPeer
))
entries.append(.demoDescription(presentationData.strings.Settings_QuickReactionSetup_DemoInfo))
entries.append(.quickReaction(presentationData.strings.Settings_QuickReactionSetup_ChooseQuickReaction, reactionSettings.quickReaction, availableReactions))
entries.append(.quickReactionDescription(presentationData.strings.Settings_QuickReactionSetup_ChooseQuickReactionInfo))
}
return entries
}
public func quickReactionSetupController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
focusOnItemTag: QuickReactionSetupEntryTag? = nil
) -> ViewController {
let statePromise = ValuePromise(QuickReactionSetupControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: QuickReactionSetupControllerState())
let updateState: ((QuickReactionSetupControllerState) -> QuickReactionSetupControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
let _ = dismissImpl
var openQuickReactionImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let arguments = QuickReactionSetupControllerArguments(
context: context,
openQuickReaction: {
openQuickReactionImpl?()
},
toggleReaction: {
updateState { state in
var state = state
state.hasReaction = !state.hasReaction
return state
}
}
)
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, let value = entry.get(ReactionSettings.self) {
reactionSettings = value
} else {
reactionSettings = .default
}
return reactionSettings
}
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(),
presentationData,
statePromise.get(),
context.engine.stickers.availableReactions(),
settings,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> deliverOnMainQueue
|> map { presentationData, state, availableReactions, settings, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = accountPeer?.isPremium ?? false
let title: String = presentationData.strings.Settings_QuickReactionSetup_Title
let entries = quickReactionSetupControllerEntries(
presentationData: presentationData,
availableReactions: availableReactions,
reactionSettings: settings,
state: state,
isPremium: isPremium,
accountPeer: accountPeer
)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(title),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
animateChanges: true
)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.alwaysSynchronous = true
openQuickReactionImpl = { [weak controller] in
let _ = (combineLatest(queue: .mainQueue(),
settings,
context.engine.stickers.availableReactions()
)
|> take(1)
|> deliverOnMainQueue).start(next: { settings, availableReactions in
var currentSelectedFileId: EngineMedia.Id?
switch settings.quickReaction {
case .builtin:
if let availableReactions = availableReactions {
if let reaction = availableReactions.reactions.first(where: { $0.value == settings.quickReaction }) {
currentSelectedFileId = reaction.selectAnimation.fileId
break
}
}
case let .custom(fileId):
currentSelectedFileId = EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)
case .stars:
if let availableReactions = availableReactions {
if let reaction = availableReactions.reactions.first(where: { $0.value == settings.quickReaction }) {
currentSelectedFileId = reaction.selectAnimation.fileId
break
}
}
}
var selectedItems = Set<EngineMedia.Id>()
if let currentSelectedFileId = currentSelectedFileId {
selectedItems.insert(currentSelectedFileId)
}
guard let controller = controller else {
return
}
var sourceItemNode: ItemListReactionItemNode?
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListReactionItemNode {
sourceItemNode = itemNode
}
}
if let sourceItemNode = sourceItemNode {
controller.present(EmojiStatusSelectionController(
context: context,
mode: .quickReactionSelection(completion: {
updateState { state in
var state = state
state.hasReaction = false
return state
}
}),
sourceView: sourceItemNode.iconView,
emojiContent: EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
subject: .quickReaction,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
selectedItems: selectedItems
),
currentSelection: nil,
destinationItemView: { [weak sourceItemNode] in
return sourceItemNode?.iconView
}
), in: .window(.root))
}
})
}
dismissImpl = { [weak controller] in
guard let controller = controller else {
return
}
controller.dismiss()
}
if focusOnItemTag == .choose {
Queue.mainQueue().after(0.1) {
arguments.openQuickReaction()
}
}
return controller
}