Files
Telegram-iOS/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LiveLocationHeaderPanelComponent.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

286 lines
15 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AccountContext
import TelegramCore
import GlobalControlPanelsContext
import SwiftSignalKit
import PresentationDataUtils
private func presentLiveLocationController(context: AccountContext, peerId: EnginePeer.Id, controller: ViewController) {
let presentImpl: (EngineMessage?) -> Void = { [weak controller] message in
if let message = message, let strongController = controller {
let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message._asMessage(), standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, modal: true, dismissInput: {
controller?.view.endEditing(true)
}, present: { c, a, _ in
controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
}, transitionNode: { _, _, _ in
return nil
}, addToTransitionSurface: { _ in
}, openUrl: { _ in
}, openPeer: { peer, navigation in
}, callPeer: { _, _ in
}, openConferenceCall: { _ in
}, enqueueMessage: { message in
let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start()
}, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in
}, chatAvatarHiddenMedia: { _, _ in
}))
}
}
if let id = context.liveLocationManager?.internalMessageForPeerId(peerId) {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: id))
|> deliverOnMainQueue).start(next: presentImpl)
} else if let liveLocationManager = context.liveLocationManager {
let _ = (liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId)
|> take(1)
|> map { peersAndMessages -> EngineMessage? in
return peersAndMessages?.first?.1
} |> deliverOnMainQueue).start(next: presentImpl)
}
}
public final class LiveLocationHeaderPanelComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let data: GlobalControlPanelsContext.LiveLocation
public let controller: () -> ViewController?
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
data: GlobalControlPanelsContext.LiveLocation,
controller: @escaping () -> ViewController?
) {
self.context = context
self.theme = theme
self.strings = strings
self.data = data
self.controller = controller
}
public static func ==(lhs: LiveLocationHeaderPanelComponent, rhs: LiveLocationHeaderPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.data != rhs.data {
return false
}
return true
}
public final class View: UIView {
private var panel: LocationBroadcastNavigationAccessoryPanel?
private var component: LiveLocationHeaderPanelComponent?
private weak var state: EmptyComponentState?
public override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: LiveLocationHeaderPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
let panel: LocationBroadcastNavigationAccessoryPanel
if let current = self.panel {
panel = current
} else {
panel = LocationBroadcastNavigationAccessoryPanel(
accountPeerId: component.context.account.peerId,
theme: component.theme,
strings: component.strings,
nameDisplayOrder: component.context.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder,
tapAction: { [weak self] in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
switch component.data.mode {
case .all:
let messages = component.data.messages.values.sorted(by: { $0.index > $1.index })
if messages.count == 1 {
presentLiveLocationController(context: component.context, peerId: messages[0].id.peerId, controller: controller)
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak actionSheet] in
actionSheet?.dismissAnimated()
}
var items: [ActionSheetItem] = []
if !messages.isEmpty {
items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(messages.count))))
for message in messages {
if let peer = message.peers[message.id.peerId] {
var beginTimeAndTimeout: (Double, Double)?
for media in message.media {
if let media = media as? TelegramMediaMap, let timeout = media.liveBroadcastingTimeout {
beginTimeAndTimeout = (Double(message.timestamp), Double(timeout))
}
}
if let beginTimeAndTimeout {
items.append(LocationBroadcastActionSheetItem(context: component.context, peer: EnginePeer(peer), title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), beginTimestamp: beginTimeAndTimeout.0, timeout: beginTimeAndTimeout.1, strings: presentationData.strings, action: { [weak self] in
dismissAction()
guard let self, let component = self.component, let controller = component.controller() else {
return
}
presentLiveLocationController(context: component.context, peerId: peer.id, controller: controller)
}))
}
}
}
items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { [weak self] in
dismissAction()
guard let self, let component = self.component else {
return
}
for peer in component.data.peers {
component.context.liveLocationManager?.cancelLiveLocation(peerId: peer.id)
}
}))
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.window?.endEditing(true)
controller.present(actionSheet, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
case let .peer(peerId):
presentLiveLocationController(context: component.context, peerId: peerId, controller: controller)
}
},
close: { [weak self] in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
var closePeers: [EnginePeer]?
var closePeerId: EnginePeer.Id?
switch component.data.mode {
case .all:
if component.data.peers.count > 1 {
closePeers = component.data.peers
} else {
closePeerId = component.data.peers.first?.id
}
case let .peer(peerId):
closePeerId = peerId
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak actionSheet] in
actionSheet?.dismissAnimated()
}
var items: [ActionSheetItem] = []
if let closePeers = closePeers, !closePeers.isEmpty {
items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(closePeers.count))))
for peer in closePeers {
items.append(ActionSheetButtonItem(title: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), action: { [weak self] in
dismissAction()
guard let self, let component = self.component, let controller = component.controller() else {
return
}
presentLiveLocationController(context: component.context, peerId: peer.id, controller: controller)
}))
}
items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { [weak self] in
dismissAction()
guard let self, let component = self.component else {
return
}
for peer in closePeers {
component.context.liveLocationManager?.cancelLiveLocation(peerId: peer.id)
}
}))
} else if let closePeerId {
items.append(ActionSheetButtonItem(title: presentationData.strings.Map_StopLiveLocation, color: .destructive, action: { [weak self] in
dismissAction()
guard let self, let component = self.component else {
return
}
component.context.liveLocationManager?.cancelLiveLocation(peerId: closePeerId)
}))
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.window?.endEditing(true)
controller.present(actionSheet, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
)
self.panel = panel
self.addSubview(panel.view)
}
let size = CGSize(width: availableSize.width, height: 40.0)
let panelFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: panel.view, frame: panelFrame)
panel.updateLayout(size: panelFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
let mappedMode: LocationBroadcastNavigationAccessoryPanelMode
switch component.data.mode {
case .all:
mappedMode = .summary
case .peer:
mappedMode = .peer
}
panel.update(peers: component.data.peers, mode: mappedMode, canClose: component.data.canClose)
if themeUpdated {
panel.updatePresentationData(PresentationData(
strings: component.strings,
theme: component.theme,
autoNightModeTriggered: false,
chatWallpaper: .builtin(WallpaperSettings()),
chatFontSize: .regular,
chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: true),
listsFontSize: .regular,
dateTimeFormat: PresentationDateTimeFormat(),
nameDisplayOrder: .firstLast,
nameSortOrder: .firstLast,
reduceMotion: false,
largeEmoji: false
))
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}