import Foundation import UIKit import Display import ComponentFlow import MultilineTextComponent import AccountContext import TelegramCore import TelegramPresentationData import SwiftSignalKit import TelegramCallsUI import AsyncListComponent import AvatarNode import ContextUI import StarsParticleEffect import StoryLiveChatMessageComponent private final class PinnedBarMessageComponent: Component { let context: AccountContext let strings: PresentationStrings let theme: PresentationTheme let message: GroupCallMessagesContext.Message let topPlace: Int? let action: () -> Void let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, message: GroupCallMessagesContext.Message, topPlace: Int?, action: @escaping () -> Void, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { self.context = context self.strings = strings self.theme = theme self.message = message self.topPlace = topPlace self.action = action self.contextGesture = contextGesture } static func ==(lhs: PinnedBarMessageComponent, rhs: PinnedBarMessageComponent) -> Bool { if lhs === rhs { return true } if lhs.context !== rhs.context { return false } if lhs.strings !== rhs.strings { return false } if lhs.theme !== rhs.theme { return false } if lhs.message != rhs.message { return false } if lhs.topPlace != rhs.topPlace { return false } if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { return false } return true } final class View: UIView { private let extractedContainerNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode private let backgroundView: UIImageView private let foregroundClippingView: UIView private let foregroundView: UIImageView private let effectLayer: StarsParticleEffectLayer private var avatarNode: AvatarNode? private let title = ComponentView() private var crownIcon: UIImageView? private var component: PinnedBarMessageComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var updateTimer: Foundation.Timer? override init(frame: CGRect) { self.extractedContainerNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.backgroundView = UIImageView() self.foregroundClippingView = UIView() self.foregroundClippingView.clipsToBounds = true self.foregroundView = UIImageView() self.effectLayer = StarsParticleEffectLayer() super.init(frame: frame) self.containerNode.addSubnode(self.extractedContainerNode) self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode self.addSubview(self.containerNode.view) self.containerNode.activated = { [weak self] gesture, _ in guard let self, let component = self.component else { return } component.contextGesture?(gesture, self.extractedContainerNode) } self.extractedContainerNode.contentNode.view.addSubview(self.backgroundView) self.foregroundClippingView.addSubview(self.foregroundView) self.extractedContainerNode.contentNode.view.addSubview(self.foregroundClippingView) self.extractedContainerNode.contentNode.view.layer.addSublayer(self.effectLayer) self.extractedContainerNode.contentNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.updateTimer?.invalidate() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { guard let component = self.component else { return } if case .ended = recognizer.state { component.action() } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } guard let result = super.hitTest(point, with: event) else { return nil } return result } func update(component: PinnedBarMessageComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } if self.updateTimer == nil { self.updateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in guard let self else { return } if !self.isUpdating { self.state?.updated(transition: .immediate, isLocal: true) } }) } let previousComponent = self.component self.component = component self.state = state self.containerNode.isGestureEnabled = component.contextGesture != nil let itemHeight: CGFloat = 32.0 let avatarInset: CGFloat = 4.0 let avatarSize: CGFloat = 24.0 let avatarSpacing: CGFloat = 6.0 let rightInset: CGFloat = 10.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.message.author?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? " ", font: Font.semibold(15.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: 200.0, height: itemHeight) ) var size = CGSize(width: avatarInset + avatarSize + avatarSpacing + titleSize.width + rightInset, height: itemHeight) if let topPlace = component.topPlace { let crownIcon: UIImageView if let current = self.crownIcon { crownIcon = current } else { crownIcon = UIImageView() self.crownIcon = crownIcon self.extractedContainerNode.contentNode.view.addSubview(crownIcon) } if topPlace != previousComponent?.topPlace { crownIcon.image = StoryLiveChatMessageComponent.generateCrownImage(place: topPlace, backgroundColor: .white, foregroundColor: .clear, borderColor: nil) } crownIcon.tintColor = .white if let image = crownIcon.image { size.width += image.size.width + 4.0 } } else { if let crownIcon = self.crownIcon { self.crownIcon = nil crownIcon.removeFromSuperview() } } if self.backgroundView.image == nil { self.backgroundView.image = generateStretchableFilledCircleImage(diameter: itemHeight, color: .white)?.withRenderingMode(.alwaysTemplate) self.foregroundView.image = self.backgroundView.image } let params = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })) let baseColor = StoryLiveChatMessageComponent.getMessageColor(color: GroupCallMessagesContext.getStarAmountParamMapping(params: params, value: component.message.paidStars ?? 0).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC)) self.backgroundView.tintColor = baseColor.withMultipliedBrightnessBy(0.7) self.foregroundView.tintColor = baseColor let timestamp = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970 let currentDuration = max(0.0, timestamp - Double(component.message.date)) var timeFraction: CGFloat = 1.0 - min(1.0, currentDuration / Double(component.message.lifetime)) if case .local = component.message.id.space { timeFraction = 1.0 } let backgroundFrame = CGRect(origin: CGPoint(), size: size) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: size)) transition.setFrame(view: self.foregroundClippingView, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * timeFraction), height: size.height))) transition.setFrame(layer: self.effectLayer, frame: CGRect(origin: CGPoint(), size: size)) self.effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: size, cornerRadius: size.height * 0.5, transition: transition) let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: floor((itemHeight - avatarSize) * 0.5)), size: CGSize(width: avatarSize, height: avatarSize)) do { let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0)) self.avatarNode = avatarNode self.extractedContainerNode.contentNode.view.addSubview(avatarNode.view) } transition.setFrame(view: avatarNode.view, frame: avatarFrame) avatarNode.updateSize(size: avatarFrame.size) if let peer = component.message.author { if peer.smallProfileImage != nil { avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) } else { avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) } } else { avatarNode.setCustomLetters([" "]) } } var titleFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((itemHeight - titleSize.height) * 0.5)), size: titleSize) if let crownIcon = self.crownIcon, let image = crownIcon.image { crownIcon.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY - 1.0), size: image.size) titleFrame.origin.x += image.size.width + 4.0 } if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() self.extractedContainerNode.contentNode.view.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -4.0, dy: 0.0) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) return CGSize(width: size.width + 10.0, height: size.height) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class PinnedBarComponent: Component { let context: AccountContext let strings: PresentationStrings let theme: PresentationTheme let isExpanded: Bool let messages: [GroupCallMessagesContext.Message] let topIndices: [EnginePeer.Id: Int] let action: (GroupCallMessagesContext.Message) -> Void let contextGesture: (GroupCallMessagesContext.Message, ContextGesture, ContextExtractedContentContainingNode) -> Void init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, isExpanded: Bool, messages: [GroupCallMessagesContext.Message], topIndices: [EnginePeer.Id: Int], action: @escaping (GroupCallMessagesContext.Message) -> Void, contextGesture: @escaping (GroupCallMessagesContext.Message, ContextGesture, ContextExtractedContentContainingNode) -> Void) { self.context = context self.strings = strings self.theme = theme self.isExpanded = isExpanded self.messages = messages self.topIndices = topIndices self.action = action self.contextGesture = contextGesture } static func ==(lhs: PinnedBarComponent, rhs: PinnedBarComponent) -> Bool { if lhs === rhs { return true } if lhs.context !== rhs.context { return false } if lhs.strings !== rhs.strings { return false } if lhs.theme !== rhs.theme { return false } if lhs.isExpanded != rhs.isExpanded { return false } if lhs.messages != rhs.messages { return false } if lhs.topIndices != rhs.topIndices { return false } return true } final class View: UIView { private let listContainer: UIView private let listState = AsyncListComponent.ExternalState() private let list = ComponentView() private var component: PinnedBarComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false override init(frame: CGRect) { self.listContainer = UIView() self.listContainer.clipsToBounds = true super.init(frame: frame) self.addSubview(self.listContainer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } guard let result = super.hitTest(point, with: event) else { return nil } return result } func update(component: PinnedBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let previousComponent = self.component self.component = component self.state = state let itemHeight: CGFloat = 32.0 let insets = UIEdgeInsets(top: 13.0, left: 20.0, bottom: 13.0, right: 20.0) let size = CGSize(width: availableSize.width, height: insets.top + itemHeight + insets.bottom) var listItems: [AnyComponentWithIdentity] = [] for message in component.messages { if let author = message.author { let id = message.id listItems.append(AnyComponentWithIdentity(id: author.id, component: AnyComponent(PinnedBarMessageComponent( context: component.context, strings: component.strings, theme: component.theme, message: message, topPlace: message.author.flatMap { component.topIndices[$0.id] }, action: { [weak self] in guard let self, let component = self.component else { return } if let message = component.messages.first(where: { $0.id == id }) { component.action(message) } }, contextGesture: message.isIncoming ? { [weak self] gesture, sourceNode in guard let self, let component = self.component else { return } if let message = component.messages.first(where: { $0.id == id }) { component.contextGesture(message, gesture, sourceNode) } else { gesture.cancel() } } : nil )))) } } let listInsets = UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right) let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) var listTransition = transition var animateIn = false if let previousComponent { if previousComponent.messages.isEmpty { listTransition = listTransition.withAnimation(.none) animateIn = true } } else { listTransition = listTransition.withAnimation(.none) animateIn = true } let _ = self.list.update( transition: listTransition, component: AnyComponent(AsyncListComponent( externalState: self.listState, items: listItems, itemSetId: AnyHashable(0), direction: .horizontal, insets: listInsets )), environment: {}, containerSize: listFrame.size ) if let listView = self.list.view { if listView.superview == nil { self.listContainer.addSubview(listView) } transition.setPosition(view: listView, position: CGRect(origin: CGPoint(), size: listFrame.size).center) transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) if animateIn { transition.animateAlpha(view: listView, from: 0.0, to: 1.0) } } transition.setFrame(view: self.listContainer, frame: listFrame) return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }