Telegram-iOS/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift
isaac d1aa0db537 Postbox -> TelegramEngine waves 46-93 (squashed)
Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits)
of the gradual Postbox->TelegramEngine consumer-side migration.

Scope: 139 files changed, 2123 insertions(+), 452 deletions(-).

## Themes by wave-block

**Waves 46-58 — Peer field migrations + facade additions**
Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer,
SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo,
ChatList, and SettingsUI components.

**Waves 59-73 — peer field cascade + EnginePeer wrap drops**
Series of single- to two-file peer-field migrations; consumer-side wrap
removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser`
cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer
convenience init. Wave 68: SelectivePrivacyPeer convenience init.

**Waves 74-83 — controller-Node bridge cleanup + small migrations**
Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of
ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem
(76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController
(78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81),
openHashtag resolveSignal (82), NotificationExceptionsList (83).

**Waves 84-90 — TelegramEngine.Resources facade migrations**
Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)`
to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep:
40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch
facade in one atomic pass with first-pass-clean build.

Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch,
resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource).

**Waves 91-92 — additional type migrations**
Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload
+ openWebSession callback Peer? -> EnginePeer?.
Wave 92: ChatListController StateHolder.EntryContext status type
MediaResourceStatus -> EngineMediaResource.FetchStatus.

**Wave 93 — speculative `import Postbox` drop sweep**
Drop import from 7 wave-touched files where it became unused; restore in 5
files where bare PeerId/Message/MediaId/StoryId references escaped the
pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in
InAppPurchaseManager to unlock its import drop.

## Build state

Final state at squash: clean Telegram/Telegram build at debug_sim_arm64.

## Persistent-state notes

- Pre-existing WIP unchanged across the squashed range:
  - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker
  - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:48:15 +04:00

239 lines
10 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Photos
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import AccountContext
import GalleryUI
import AppBundle
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
private let nameFont = Font.medium(15.0)
private let dateFont = Font.regular(14.0)
enum AvatarGalleryItemFooterContent {
case info
case own(Bool)
}
final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
private let context: AccountContext
private var presentationData: PresentationData
private var strings: PresentationStrings
private var dateTimeFormat: PresentationDateTimeFormat
private let deleteButton: UIButton
private let actionButton: UIButton
private let nameNode: ASTextNode
private let dateNode: ASTextNode
private let mainNode: ASTextNode
private let setMainButton: HighlightableButtonNode
private var currentNameText: String?
private var currentDateText: String?
private var currentTypeText: String?
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat, CGFloat)?
var delete: (() -> Void)? {
didSet {
self.deleteButton.isHidden = self.delete == nil
}
}
var share: ((GalleryControllerInteraction) -> Void)?
var setMain: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData) {
self.context = context
self.presentationData = presentationData
self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat
self.deleteButton = UIButton()
self.deleteButton.isHidden = true
self.actionButton = UIButton()
self.deleteButton.setImage(deleteImage, for: [.normal])
self.actionButton.setImage(actionImage, for: [.normal])
self.nameNode = ASTextNode()
self.nameNode.maximumNumberOfLines = 1
self.nameNode.isUserInteractionEnabled = false
self.nameNode.displaysAsynchronously = false
self.dateNode = ASTextNode()
self.dateNode.maximumNumberOfLines = 1
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = false
self.setMainButton = HighlightableButtonNode()
self.setMainButton.isHidden = true
self.mainNode = ASTextNode()
self.mainNode.maximumNumberOfLines = 1
self.mainNode.isUserInteractionEnabled = false
self.mainNode.displaysAsynchronously = false
super.init()
self.view.addSubview(self.deleteButton)
self.view.addSubview(self.actionButton)
self.addSubnode(self.nameNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.setMainButton)
self.addSubnode(self.mainNode)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
self.setMainButton.addTarget(self, action: #selector(self.setMainButtonPressed), forControlEvents: .touchUpInside)
}
func setEntry(_ entry: AvatarGalleryEntry, content: AvatarGalleryItemFooterContent) {
var nameText: String?
var dateText: String?
var typeText: String?
var buttonText: String?
var canShare = true
switch entry {
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback, _):
if date != 0 || isFallback {
nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
}
if let date = date, date != 0 {
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string
} else if isFallback {
dateText = !videoRepresentations.isEmpty ? self.strings.ProfilePhoto_PublicVideo : self.strings.ProfilePhoto_PublicPhoto
}
if (!videoRepresentations.isEmpty) {
typeText = self.strings.ProfilePhoto_MainVideo
buttonText = self.strings.ProfilePhoto_SetMainVideo
} else {
typeText = self.strings.ProfilePhoto_MainPhoto
buttonText = self.strings.ProfilePhoto_SetMainPhoto
}
if let peer = peer {
canShare = !peer.isCopyProtectionEnabled
}
default:
break
}
if self.currentNameText != nameText || self.currentDateText != dateText {
self.currentNameText = nameText
self.currentDateText = dateText
if let nameText = nameText {
self.nameNode.attributedText = NSAttributedString(string: nameText, font: nameFont, textColor: .white)
} else {
self.nameNode.attributedText = nil
}
if let dateText = dateText {
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
} else {
self.dateNode.attributedText = nil
}
}
if self.currentTypeText != typeText {
self.currentTypeText = typeText
self.mainNode.attributedText = NSAttributedString(string: typeText ?? "", font: Font.regular(17.0), textColor: UIColor(rgb: 0x808080))
self.setMainButton.setAttributedTitle(NSAttributedString(string: buttonText ?? "", font: Font.regular(17.0), textColor: .white), for: .normal)
if let validLayout = self.validLayout {
let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: .immediate)
}
}
self.actionButton.isHidden = !canShare
switch content {
case .info:
self.nameNode.isHidden = false
self.dateNode.isHidden = false
self.mainNode.isHidden = true
self.setMainButton.isHidden = true
case let .own(isMainPhoto):
self.nameNode.isHidden = true
self.dateNode.isHidden = true
self.mainNode.isHidden = !isMainPhoto
self.setMainButton.isHidden = isMainPhoto
}
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> LayoutInfo {
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset, contentInset)
let width = size.width
var panelHeight: CGFloat = 44.0 + bottomInset
panelHeight += contentInset
self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
let constrainedSize = CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)
let nameSize = self.nameNode.measure(constrainedSize)
let dateSize = self.dateNode.measure(constrainedSize)
if nameSize.height.isZero {
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)
} else {
let labelsSpacing: CGFloat = 0.0
self.nameNode.frame = CGRect(origin: CGPoint(x: floor((width - nameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - nameSize.height - labelsSpacing) / 2.0)), size: nameSize)
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - nameSize.height - labelsSpacing) / 2.0) + nameSize.height + labelsSpacing), size: dateSize)
}
let mainSize = self.mainNode.measure(constrainedSize)
self.mainNode.frame = CGRect(origin: CGPoint(x: floor((width - mainSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - mainSize.height) / 2.0)), size: mainSize)
let mainButtonSize = self.setMainButton.measure(constrainedSize)
self.setMainButton.frame = CGRect(origin: CGPoint(x: floor((width - mainButtonSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - mainButtonSize.height) / 2.0)), size: mainButtonSize)
return LayoutInfo(height: panelHeight, needsShadow: false)
}
override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
self.deleteButton.alpha = 1.0
self.actionButton.alpha = 1.0
self.nameNode.alpha = 1.0
self.dateNode.alpha = 1.0
self.setMainButton.alpha = 1.0
}
override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
self.deleteButton.alpha = 0.0
self.actionButton.alpha = 0.0
self.nameNode.alpha = 0.0
self.dateNode.alpha = 0.0
self.setMainButton.alpha = 0.0
completion()
}
@objc private func deleteButtonPressed() {
self.delete?()
}
@objc private func actionButtonPressed() {
if let controllerInteraction = self.controllerInteraction {
self.share?(controllerInteraction)
}
}
@objc private func setMainButtonPressed() {
self.setMain?()
}
}