import Foundation import UIKit import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import AccountContext import AttachmentTextInputPanelNode import ChatPresentationInterfaceState import ChatSendMessageActionUI import ChatTextLinkEditUI import PhotoResources import AnimatedStickerComponent import SemanticStatusNode import MediaResources import MultilineTextComponent import ShimmerEffect import TextFormat import LegacyMessageInputPanel import LegacyMessageInputPanelInputView private let buttonSize = CGSize(width: 88.0, height: 49.0) private let smallButtonWidth: CGFloat = 69.0 private let iconSize = CGSize(width: 30.0, height: 30.0) private let sideInset: CGFloat = 3.0 private final class IconComponent: Component { public let account: Account public let name: String public let fileReference: FileMediaReference? public let animationName: String? public let tintColor: UIColor? public init(account: Account, name: String, fileReference: FileMediaReference?, animationName: String?, tintColor: UIColor?) { self.account = account self.name = name self.fileReference = fileReference self.animationName = animationName self.tintColor = tintColor } public static func ==(lhs: IconComponent, rhs: IconComponent) -> Bool { if lhs.account !== rhs.account { return false } if lhs.name != rhs.name { return false } if lhs.fileReference?.media != rhs.fileReference?.media { return false } if lhs.animationName != rhs.animationName { return false } if lhs.tintColor != rhs.tintColor { return false } return false } public final class View: UIImageView { private var component: IconComponent? private var disposable: Disposable? override init(frame: CGRect) { super.init(frame: frame) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.disposable?.dispose() } func update(component: IconComponent, availableSize: CGSize, transition: Transition) -> CGSize { if self.component?.name != component.name || self.component?.fileReference?.media.fileId != component.fileReference?.media.fileId || self.component?.tintColor != component.tintColor { if let fileReference = component.fileReference { let previousName = self.component?.name ?? "" if !previousName.isEmpty { self.image = nil } self.disposable = (svgIconImageFile(account: component.account, fileReference: fileReference) |> runOn(Queue.concurrentDefaultQueue()) |> deliverOnMainQueue).startStrict(next: { [weak self] transform in let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: availableSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets()) let drawingContext = transform(arguments) let image = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate) if let tintColor = component.tintColor { self?.image = generateTintedImage(image: image, color: tintColor, backgroundColor: nil) } else { self?.image = image } }).strict() } else { if let tintColor = component.tintColor { self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) } else { self.image = UIImage(bundleImageName: component.name) } } } self.component = component return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } private final class AttachButtonComponent: CombinedComponent { let context: AccountContext let type: AttachmentButtonType let isSelected: Bool let strings: PresentationStrings let theme: PresentationTheme let action: () -> Void let longPressAction: () -> Void init( context: AccountContext, type: AttachmentButtonType, isSelected: Bool, strings: PresentationStrings, theme: PresentationTheme, action: @escaping () -> Void, longPressAction: @escaping () -> Void ) { self.context = context self.type = type self.isSelected = isSelected self.strings = strings self.theme = theme self.action = action self.longPressAction = longPressAction } static func ==(lhs: AttachButtonComponent, rhs: AttachButtonComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.type != rhs.type { return false } if lhs.isSelected != rhs.isSelected { return false } if lhs.strings !== rhs.strings { return false } if lhs.theme !== rhs.theme { return false } return true } static var body: Body { let icon = Child(IconComponent.self) let animatedIcon = Child(AnimatedStickerComponent.self) let title = Child(MultilineTextComponent.self) let button = Child(Rectangle.self) return { context in let name: String let imageName: String var imageFile: TelegramMediaFile? var animationFile: TelegramMediaFile? var botPeer: EnginePeer? let component = context.component let strings = component.strings switch component.type { case .gallery: name = strings.Attachment_Gallery imageName = "Chat/Attach Menu/Gallery" case .file: name = strings.Attachment_File imageName = "Chat/Attach Menu/File" case .location: name = strings.Attachment_Location imageName = "Chat/Attach Menu/Location" case .contact: name = strings.Attachment_Contact imageName = "Chat/Attach Menu/Contact" case .poll: name = strings.Attachment_Poll imageName = "Chat/Attach Menu/Poll" case .gift: name = strings.Attachment_Gift imageName = "Chat/Attach Menu/Gift" case let .app(bot): botPeer = bot.peer name = bot.shortName imageName = "" if let file = bot.icons[.iOSAnimated] { animationFile = file } else if let file = bot.icons[.iOSStatic] { imageFile = file } else if let file = bot.icons[.default] { imageFile = file } case .standalone: name = "" imageName = "" imageFile = nil case .quickReply: name = strings.Attachment_Reply imageName = "Chat/Attach Menu/Reply" } let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor let iconSize = CGSize(width: 30.0, height: 30.0) let topInset: CGFloat = 4.0 + UIScreenPixel let spacing: CGFloat = 15.0 + UIScreenPixel let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize) if let animationFile = animationFile { let icon = animatedIcon.update( component: AnimatedStickerComponent( account: component.context.account, animation: AnimatedStickerComponent.Animation( source: .file(media: animationFile), scale: UIScreenScale, loop: false ), tintColor: tintColor, isAnimating: component.isSelected, size: CGSize(width: iconSize.width, height: iconSize.height) ), availableSize: iconSize, transition: context.transition ) context.add(icon .position(CGPoint(x: iconFrame.midX, y: iconFrame.midY)) ) } else { var fileReference: FileMediaReference? if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile { fileReference = .attachBot(peer: peer, media: imageFile) } let icon = icon.update( component: IconComponent( account: component.context.account, name: imageName, fileReference: fileReference, animationName: nil, tintColor: tintColor ), availableSize: iconSize, transition: context.transition ) context.add(icon .position(CGPoint(x: iconFrame.midX, y: iconFrame.midY)) ) } let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: name, font: Font.regular(10.0), textColor: context.component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, paragraphAlignment: .center)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 ), availableSize: context.availableSize, transition: .immediate ) let button = button.update( component: Rectangle( color: .clear, width: context.availableSize.width, height: context.availableSize.height ), availableSize: context.availableSize, transition: .immediate ) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - title.size.width) / 2.0), y: iconFrame.midY + spacing), size: title.size) context.add(title .position(CGPoint(x: titleFrame.midX, y: titleFrame.midY)) ) context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .gesture(.tap { component.action() }) .gesture(.longPress({ state in if case .began = state { component.longPressAction() } })) ) return context.availableSize } } } private final class LoadingProgressNode: ASDisplayNode { var color: UIColor { didSet { self.foregroundNode.backgroundColor = self.color } } private let foregroundNode: ASDisplayNode init(color: UIColor) { self.color = color self.foregroundNode = ASDisplayNode() self.foregroundNode.backgroundColor = color super.init() self.addSubnode(self.foregroundNode) } private var _progress: CGFloat = 0.0 func updateProgress(_ progress: CGFloat, animated: Bool = false) { if self._progress == progress && animated { return } var animated = animated if (progress < self._progress && animated) { animated = false } let size = self.bounds.size self._progress = progress let transition: ContainedViewLayoutTransition if animated && progress > 0.0 { transition = .animated(duration: 0.7, curve: .spring) } else { transition = .immediate } let alpaTransition: ContainedViewLayoutTransition if animated { alpaTransition = .animated(duration: 0.3, curve: .easeInOut) } else { alpaTransition = .immediate } transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height)) let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0 alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha) } override func layout() { super.layout() self.foregroundNode.cornerRadius = self.frame.height / 2.0 } } private final class MainButtonNode: HighlightTrackingButtonNode { private var state: AttachmentMainButtonState private var size: CGSize? private let backgroundAnimationNode: ASImageNode fileprivate let textNode: ImmediateTextNode private let statusNode: SemanticStatusNode private var progressNode: ASImageNode? private var shimmerView: ShimmerEffectForegroundView? private var borderView: UIView? private var borderMaskView: UIView? private var borderShimmerView: ShimmerEffectForegroundView? override init(pointerStyle: PointerStyle? = nil) { self.state = AttachmentMainButtonState.initial self.backgroundAnimationNode = ASImageNode() self.backgroundAnimationNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.textAlignment = .center self.textNode.displaysAsynchronously = false self.statusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) super.init(pointerStyle: pointerStyle) self.isExclusiveTouch = true self.clipsToBounds = true self.addSubnode(self.backgroundAnimationNode) self.addSubnode(self.textNode) self.addSubnode(self.statusNode) self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self, strongSelf.state.isEnabled { if highlighted { strongSelf.layer.removeAnimation(forKey: "opacity") strongSelf.alpha = 0.65 } else { strongSelf.alpha = 1.0 strongSelf.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) } } } } override func didLoad() { super.didLoad() self.cornerRadius = 12.0 if #available(iOS 13.0, *) { self.layer.cornerCurve = .continuous } } public func transitionToProgress() { guard self.progressNode == nil, let size = self.size else { return } self.isUserInteractionEnabled = false let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) rotationAnimation.duration = 1.0 rotationAnimation.fromValue = NSNumber(value: Float(0.0)) rotationAnimation.toValue = NSNumber(value: Float.pi * 2.0) rotationAnimation.repeatCount = Float.infinity rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) rotationAnimation.beginTime = 1.0 let buttonOffset: CGFloat = 0.0 let buttonWidth = size.width let progressNode = ASImageNode() let diameter: CGFloat = size.height - 22.0 let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((size.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) progressNode.frame = progressFrame progressNode.image = generateIndefiniteActivityIndicatorImage(color: .white, diameter: diameter, lineWidth: 3.0) self.addSubnode(progressNode) progressNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) progressNode.layer.add(rotationAnimation, forKey: "progressRotation") self.progressNode = progressNode self.textNode.alpha = 0.0 self.textNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2) self.shimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.borderShimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } public func transitionFromProgress() { guard let progressNode = self.progressNode else { return } self.progressNode = nil progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode, weak self] _ in progressNode?.removeFromSupernode() self?.isUserInteractionEnabled = true }) self.textNode.alpha = 1.0 self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.shimmerView?.layer.removeAllAnimations() self.shimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.borderShimmerView?.layer.removeAllAnimations() self.borderShimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } private func setupShimmering() { if case .premium = self.state.background { if self.shimmerView == nil { let shimmerView = ShimmerEffectForegroundView() shimmerView.isUserInteractionEnabled = false self.shimmerView = shimmerView shimmerView.layer.cornerRadius = 12.0 if #available(iOS 13.0, *) { shimmerView.layer.cornerCurve = .continuous } let borderView = UIView() borderView.isUserInteractionEnabled = false self.borderView = borderView let borderMaskView = UIView() borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel borderMaskView.layer.borderColor = UIColor.white.cgColor borderMaskView.layer.cornerRadius = 12.0 borderView.mask = borderMaskView self.borderMaskView = borderMaskView let borderShimmerView = ShimmerEffectForegroundView() self.borderShimmerView = borderShimmerView borderView.addSubview(borderShimmerView) self.view.addSubview(shimmerView) self.view.addSubview(borderView) self.updateShimmerParameters() if let size = self.size { self.updateLayout(size: size, state: state, transition: .immediate) } } } else if self.shimmerView != nil { self.shimmerView?.removeFromSuperview() self.borderView?.removeFromSuperview() self.borderMaskView?.removeFromSuperview() self.borderShimmerView?.removeFromSuperview() self.shimmerView = nil self.borderView = nil self.borderMaskView = nil self.borderShimmerView = nil } } func updateShimmerParameters() { guard let shimmerView = self.shimmerView, let borderShimmerView = self.borderShimmerView else { return } let color = UIColor.white let alpha: CGFloat let borderAlpha: CGFloat let compositingFilter: String? if color.lightness > 0.5 { alpha = 0.5 borderAlpha = 0.75 compositingFilter = "overlayBlendMode" } else { alpha = 0.2 borderAlpha = 0.3 compositingFilter = nil } shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true) borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true) shimmerView.layer.compositingFilter = compositingFilter borderShimmerView.layer.compositingFilter = compositingFilter } private func setupGradientAnimations() { if let _ = self.backgroundAnimationNode.layer.animation(forKey: "movement") { } else { let offset = (self.backgroundAnimationNode.frame.width - self.frame.width) / 2.0 let previousValue = self.backgroundAnimationNode.position.x var newValue: CGFloat = offset if offset - previousValue < self.backgroundAnimationNode.frame.width * 0.25 { newValue -= self.backgroundAnimationNode.frame.width * 0.35 } self.backgroundAnimationNode.position = CGPoint(x: newValue, y: self.backgroundAnimationNode.bounds.size.height / 2.0) CATransaction.begin() let animation = CABasicAnimation(keyPath: "position.x") animation.duration = 4.5 animation.fromValue = previousValue animation.toValue = newValue animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) CATransaction.setCompletionBlock { [weak self] in self?.setupGradientAnimations() } self.backgroundAnimationNode.layer.add(animation, forKey: "movement") CATransaction.commit() } } func updateLayout(size: CGSize, state: AttachmentMainButtonState, transition: ContainedViewLayoutTransition) { let previousState = self.state self.state = state self.size = size self.isUserInteractionEnabled = state.isVisible self.setupShimmering() if let text = state.text { let font: UIFont switch state.font { case .regular: font = Font.regular(17.0) case .bold: font = Font.semibold(17.0) } self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: state.textColor) let textSize = self.textNode.updateLayout(size) self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) switch state.background { case let .color(backgroundColor): self.backgroundAnimationNode.image = nil self.backgroundAnimationNode.layer.removeAllAnimations() self.backgroundColor = backgroundColor case .premium: if self.backgroundAnimationNode.image == nil { let backgroundColors = [ UIColor(rgb: 0x0077ff), UIColor(rgb: 0x6b93ff), UIColor(rgb: 0x8878ff), UIColor(rgb: 0xe46ace) ] var locations: [CGFloat] = [] let delta = 1.0 / CGFloat(backgroundColors.count - 1) for i in 0 ..< backgroundColors.count { locations.append(delta * CGFloat(i)) } self.backgroundAnimationNode.image = generateGradientImage(size: CGSize(width: 200.0, height: 50.0), colors: backgroundColors, locations: locations, direction: .horizontal) self.backgroundAnimationNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width * 2.4, height: size.height)) if self.backgroundAnimationNode.layer.animation(forKey: "movement") == nil { self.backgroundAnimationNode.position = CGPoint(x: size.width * 2.4 / 2.0 - self.backgroundAnimationNode.frame.width * 0.35, y: size.height / 2.0) } self.setupGradientAnimations() } self.backgroundColor = UIColor(rgb: 0x8878ff) } } if previousState.progress != state.progress { if state.progress == .center { self.transitionToProgress() } else { self.transitionFromProgress() } } if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView { let buttonFrame = CGRect(origin: .zero, size: size) let buttonWidth = size.width let buttonHeight = size.height transition.updateFrame(view: shimmerView, frame: buttonFrame) transition.updateFrame(view: borderView, frame: buttonFrame) transition.updateFrame(view: borderMaskView, frame: buttonFrame) transition.updateFrame(view: borderShimmerView, frame: buttonFrame) shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: buttonWidth * 4.0, y: 0.0), size: size), within: CGSize(width: buttonWidth * 9.0, height: buttonHeight)) borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: buttonWidth * 4.0, y: 0.0), size: size), within: CGSize(width: buttonWidth * 9.0, height: buttonHeight)) } let statusSize = CGSize(width: 20.0, height: 20.0) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: size.width - statusSize.width - 15.0, y: floorToScreenPixels((size.height - statusSize.height) / 2.0)), size: statusSize)) self.statusNode.foregroundNodeColor = state.textColor self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0)) : .none) } } final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let isScheduledMessages: Bool private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var iconDisposables: [MediaId: Disposable] = [:] private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? private let containerNode: ASDisplayNode private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let separatorNode: ASDisplayNode private var buttonViews: [Int: ComponentHostView] = [:] private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? private var mainButtonNode: MainButtonNode private var loadingProgress: CGFloat? private var mainButtonState: AttachmentMainButtonState = .initial private var elevateProgress: Bool = false private var buttons: [AttachmentButtonType] = [] private var selectedIndex: Int = 0 private(set) var isSelecting: Bool = false private var _isButtonVisible: Bool = false var isButtonVisible: Bool { return self.mainButtonState.isVisible } private var validLayout: ContainerViewLayout? private var scrollLayout: (width: CGFloat, contentSize: CGSize)? var fromMenu: Bool = false var isStandalone: Bool = false var selectionChanged: (AttachmentButtonType) -> Bool = { _ in return false } var longPressed: (AttachmentButtonType) -> Void = { _ in } var beganTextEditing: () -> Void = {} var textUpdated: (NSAttributedString) -> Void = { _ in } var sendMessagePressed: (AttachmentTextInputPanelSendMode) -> Void = { _ in } var requestLayout: () -> Void = {} var present: (ViewController) -> Void = { _ in } var presentInGlobalOverlay: (ViewController) -> Void = { _ in } var mainButtonPressed: () -> Void = { } init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.isScheduledMessages = isScheduledMessages self.makeEntityInputView = makeEntityInputView self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) self.containerNode = ASDisplayNode() self.containerNode.clipsToBounds = true self.scrollNode = ASScrollNode() self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor) self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor self.mainButtonNode = MainButtonNode() super.init() self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.separatorNode) self.containerNode.addSubnode(self.scrollNode) self.addSubnode(self.mainButtonNode) self.mainButtonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in }, deleteSelectedMessages: { }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) }, forwardSelectedMessages: { }, forwardCurrentForwardMessages: { }, forwardMessages: { _ in }, updateForwardOptionsState: { [weak self] value in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState($0.forwardOptionsState) }) }) } }, presentForwardOptions: { _ in }, presentReplyOptions: { _ in }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode) return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(updatedState) }.updatedInputMode({ _ in updatedMode }) }) } }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in var value = value value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId return value }) }) }) } }, openStickers: { }, editMessage: { }, beginMessageSearch: { _, _ in }, dismissMessageSearch: { }, updateMessageSearch: { _ in }, openSearchResults: { }, navigateMessageSearch: { _ in }, openCalendarSearch: { }, toggleMembersSearch: { _ in }, navigateToMessage: { _, _, _, _ in }, navigateToChat: { _ in }, navigateToProfile: { _ in }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in }, sendShortcut: { _ in }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in }, finishMediaRecording: { _ in }, stopMediaRecording: { }, lockMediaRecording: { }, resumeMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _, _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in }, unpinMessage: { _, _, _ in }, unpinAllMessages: { }, openPinnedList: { _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { }, dismissReportPeer: { }, deleteChat: { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { }, requestUnvoteInMessage: { _ in }, requestStopPollInMessage: { _ in }, updateInputLanguage: { _ in }, unarchiveChat: { }, openLinkEditing: { [weak self] in if let strongSelf = self { var selectionRange: Range? var text: NSAttributedString? var inputMode: ChatInputMode? strongSelf.updateChatPresentationInterfaceState(animated: true, { state in selectionRange = state.interfaceState.effectiveInputState.selectionRange if let selectionRange = selectionRange { text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) } inputMode = state.inputMode return state }) var link: String? if let text { text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { link = linkAttribute.url } } } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in return state.updatedInterfaceState({ $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) }) }) } if let textInputPanelNode = strongSelf.textInputPanelNode { textInputPanelNode.ensureFocused() } strongSelf.updateChatPresentationInterfaceState(animated: true, { state in return state.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) }) }) } }) strongSelf.present(controller) } }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { return } textInputPanelNode.loadTextInputNodeIfNeeded() guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else { return } var hasEntityKeyboard = false if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } let _ = (strongSelf.context.account.viewTracker.peerView(peerId) |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { return } var sendWhenOnlineAvailable = false if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if currentTime > until { sendWhenOnlineAvailable = true } } if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { sendWhenOnlineAvailable = false } let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: textInputPanelNode?.sendMessage(.generic) case .silently: textInputPanelNode?.sendMessage(.silent) case .whenOnline: textInputPanelNode?.sendMessage(.whenOnline) } }, schedule: { [weak textInputPanelNode] in textInputPanelNode?.sendMessage(.schedule) }) controller.emojiViewProvider = textInputPanelNode.emojiViewProvider strongSelf.presentInGlobalOverlay(controller) }) }, openScheduledMessages: { }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { }, viewReplies: { _, _ in }, activatePinnedListPreview: { _, _ in }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in }, openInviteRequests: { }, openSendAsPeer: { _, _ in }, presentChatRequestAdminInfo: { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in }, insertText: { _ in }, backwardsDeleteText: { }, restartTopic: { }, toggleTranslation: { _ in }, changeTranslationLanguage: { _ in }, addDoNotTranslateLanguage: { _ in }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { }, openBoostToUnrestrict: { }, updateVideoTrimRange: { _, _, _, _ in }, updateHistoryFilter: { _ in }, updateDisplayHistoryFilterAsList: { _ in }, requestLayout: { _ in }, chatController: { return nil }, statuses: nil) self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData strongSelf.backgroundNode.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) strongSelf.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) }) if let layout = strongSelf.validLayout { let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, elevateProgress: strongSelf.elevateProgress, transition: .immediate) } } }).strict() } deinit { self.presentationDataDisposable?.dispose() for (_, disposable) in self.iconDisposables { disposable.dispose() } } override func didLoad() { super.didLoad() if #available(iOS 13.0, *) { self.containerNode.layer.cornerCurve = .continuous } self.scrollNode.view.delegate = self self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false self.view.accessibilityTraits = .tabBar } @objc private func buttonPressed() { self.mainButtonPressed() } func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateAlpha(node: self.separatorNode, alpha: alpha) transition.updateAlpha(node: self.backgroundNode, alpha: alpha) } func updateCaption(_ caption: NSAttributedString) { if !caption.string.isEmpty { self.loadTextNodeIfNeeded() } self.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withUpdatedComposeInputState(ChatTextInputState(inputText: caption))} }) } private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState self.presentationInterfaceState = presentationInterfaceState if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) } } func updateSelectedIndex(_ index: Int) { self.selectedIndex = index self.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) } func updateViews(transition: Transition) { guard let layout = self.validLayout else { return } let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0) var validButtons = Set() var distanceBetweenNodes = layout.size.width / CGFloat(self.buttons.count) let internalWidth = distanceBetweenNodes * CGFloat(self.buttons.count - 1) var leftNodeOriginX = (layout.size.width - internalWidth) / 2.0 var buttonWidth = buttonSize.width if self.buttons.count > 6 && layout.size.width < layout.size.height { buttonWidth = smallButtonWidth distanceBetweenNodes = buttonWidth leftNodeOriginX = layout.safeInsets.left + sideInset + buttonWidth / 2.0 } for i in 0 ..< self.buttons.count { let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonWidth / 2.0) let buttonFrame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: CGSize(width: buttonWidth, height: buttonSize.height)) if !visibleRect.intersects(buttonFrame) { continue } validButtons.insert(i) var buttonTransition = transition let buttonView: ComponentHostView if let current = self.buttonViews[i] { buttonView = current } else { buttonTransition = .immediate buttonView = ComponentHostView() self.buttonViews[i] = buttonView self.scrollNode.view.addSubview(buttonView) } let type = self.buttons[i] if case let .app(bot) = type { for (name, file) in bot.icons { if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) { if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer._asPeer()) { if case .placeholder = name { let account = self.context.account let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } self.iconDisposables[file.fileId] = accountFullSizeData.start() } } else { self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).startStrict() } } } } } let _ = buttonView.update( transition: buttonTransition, component: AnyComponent(AttachButtonComponent( context: self.context, type: type, isSelected: i == self.selectedIndex, strings: self.presentationData.strings, theme: self.presentationData.theme, action: { [weak self] in if let strongSelf = self { if strongSelf.selectionChanged(type) { strongSelf.selectedIndex = i strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) if strongSelf.buttons.count > 6, let button = strongSelf.buttonViews[i] { strongSelf.scrollNode.view.scrollRectToVisible(button.frame.insetBy(dx: -35.0, dy: 0.0), animated: true) } } } }, longPressAction: { [weak self] in if let strongSelf = self, i == strongSelf.selectedIndex { strongSelf.longPressed(type) } }) ), environment: {}, containerSize: CGSize(width: buttonWidth, height: buttonSize.height) ) buttonTransition.setFrame(view: buttonView, frame: buttonFrame) var accessibilityTitle = "" switch type { case .gallery: accessibilityTitle = self.presentationData.strings.Attachment_Gallery case .file: accessibilityTitle = self.presentationData.strings.Attachment_File case .location: accessibilityTitle = self.presentationData.strings.Attachment_Location case .contact: accessibilityTitle = self.presentationData.strings.Attachment_Contact case .poll: accessibilityTitle = self.presentationData.strings.Attachment_Poll case .gift: accessibilityTitle = self.presentationData.strings.Attachment_Gift case let .app(bot): accessibilityTitle = bot.shortName case .standalone: accessibilityTitle = "" case .quickReply: accessibilityTitle = self.presentationData.strings.Attachment_Reply } buttonView.isAccessibilityElement = true buttonView.accessibilityLabel = accessibilityTitle buttonView.accessibilityTraits = [.button] } } private func updateScrollLayoutIfNeeded(force: Bool, transition: ContainedViewLayoutTransition) -> Bool { guard let layout = self.validLayout else { return false } if self.scrollLayout?.width == layout.size.width && !force { return false } var contentSize = CGSize(width: layout.size.width, height: buttonSize.height) var buttonWidth = buttonSize.width if self.buttons.count > 6 && layout.size.width < layout.size.height { buttonWidth = smallButtonWidth contentSize.width = layout.safeInsets.left + layout.safeInsets.right + sideInset * 2.0 + CGFloat(self.buttons.count) * buttonWidth } self.scrollLayout = (layout.size.width, contentSize) transition.updateFrameAsPositionAndBounds(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSelecting || self._isButtonVisible ? -buttonSize.height : 0.0), size: CGSize(width: layout.size.width, height: buttonSize.height))) self.scrollNode.view.contentSize = contentSize return true } private func loadTextNodeIfNeeded() { if let _ = self.textInputPanelNode { } else { let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in if let strongSelf = self { strongSelf.present(c) } }, makeEntityInputView: self.makeEntityInputView) textInputPanelNode.interfaceInteraction = self.interfaceInteraction textInputPanelNode.sendMessage = { [weak self] mode in if let strongSelf = self { strongSelf.sendMessagePressed(mode) } } textInputPanelNode.focusUpdated = { [weak self] focus in if let strongSelf = self, focus { strongSelf.beganTextEditing() } } textInputPanelNode.updateHeight = { [weak self] _ in if let strongSelf = self { strongSelf.requestLayout() } } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0 textInputPanelNode.isUserInteractionEnabled = self.isSelecting } } func updateLoadingProgress(_ progress: CGFloat?) { self.loadingProgress = progress } func updateMainButtonState(_ mainButtonState: AttachmentMainButtonState?) { var currentButtonState = self.mainButtonState if mainButtonState == nil { currentButtonState = AttachmentMainButtonState(text: currentButtonState.text, font: currentButtonState.font, background: currentButtonState.background, textColor: currentButtonState.textColor, isVisible: false, progress: .none, isEnabled: currentButtonState.isEnabled) } self.mainButtonState = mainButtonState ?? currentButtonState } let animatingTransitionPromise = ValuePromise(false) private(set) var animatingTransition = false { didSet { self.animatingTransitionPromise.set(self.animatingTransition) } } func animateTransitionIn(inputTransition: AttachmentController.InputPanelTransition, transition: ContainedViewLayoutTransition) { guard !self.animatingTransition, let inputNodeSnapshotView = inputTransition.inputNode.view.snapshotView(afterScreenUpdates: false) else { return } guard let menuIconSnapshotView = inputTransition.menuIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = inputTransition.menuTextNode.view.snapshotView(afterScreenUpdates: false) else { return } self.animatingTransition = true let targetButtonColor = self.mainButtonNode.backgroundColor self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundNode.backgroundColor transition.updateBackgroundColor(node: self.mainButtonNode, color: targetButtonColor ?? .clear) transition.animateFrame(layer: self.mainButtonNode.layer, from: inputTransition.menuButtonNode.frame) transition.animatePosition(node: self.mainButtonNode.textNode, from: CGPoint(x: inputTransition.menuButtonNode.frame.width / 2.0, y: inputTransition.menuButtonNode.frame.height / 2.0)) let targetButtonCornerRadius = self.mainButtonNode.cornerRadius self.mainButtonNode.cornerRadius = inputTransition.menuButtonNode.cornerRadius transition.updateCornerRadius(node: self.mainButtonNode, cornerRadius: targetButtonCornerRadius) self.mainButtonNode.subnodeTransform = CATransform3DMakeScale(0.2, 0.2, 1.0) transition.updateSublayerTransformScale(node: self.mainButtonNode, scale: 1.0) self.mainButtonNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let menuContentDelta = (self.mainButtonNode.frame.width - inputTransition.menuButtonNode.frame.width) / 2.0 menuIconSnapshotView.frame = inputTransition.menuIconNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX, dy: inputTransition.menuButtonNode.frame.minY) self.view.addSubview(menuIconSnapshotView) menuIconSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuIconSnapshotView] _ in menuIconSnapshotView?.removeFromSuperview() }) transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x + menuContentDelta, y: self.mainButtonNode.position.y)) menuTextSnapshotView.frame = inputTransition.menuTextNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + 19.0, dy: inputTransition.menuButtonNode.frame.minY) self.view.addSubview(menuTextSnapshotView) menuTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuTextSnapshotView] _ in menuTextSnapshotView?.removeFromSuperview() }) transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.mainButtonNode.position.y)) inputNodeSnapshotView.clipsToBounds = true inputNodeSnapshotView.contentMode = .right inputNodeSnapshotView.frame = CGRect(x: inputTransition.menuButtonNode.frame.maxX, y: 0.0, width: inputNodeSnapshotView.frame.width - inputTransition.menuButtonNode.frame.maxX, height: inputNodeSnapshotView.frame.height) self.view.addSubview(inputNodeSnapshotView) let targetInputPosition = CGPoint(x: inputNodeSnapshotView.center.x + inputNodeSnapshotView.frame.width, y: self.mainButtonNode.position.y) transition.updatePosition(layer: inputNodeSnapshotView.layer, position: targetInputPosition, completion: { [weak inputNodeSnapshotView, weak self] _ in inputNodeSnapshotView?.removeFromSuperview() self?.animatingTransition = false }) } private var dismissed = false func animateTransitionOut(inputTransition: AttachmentController.InputPanelTransition, dismissed: Bool, transition: ContainedViewLayoutTransition) { guard !self.animatingTransition, let inputNodeSnapshotView = inputTransition.inputNode.view.snapshotView(afterScreenUpdates: false) else { return } if dismissed { inputTransition.prepareForDismiss() } self.animatingTransition = true self.dismissed = dismissed let action = { guard let menuIconSnapshotView = inputTransition.menuIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = inputTransition.menuTextNode.view.snapshotView(afterScreenUpdates: false) else { return } let sourceButtonColor = self.mainButtonNode.backgroundColor transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundNode.backgroundColor ?? .clear) let sourceButtonFrame = self.mainButtonNode.frame transition.updateFrame(node: self.mainButtonNode, frame: inputTransition.menuButtonNode.frame) let sourceButtonTextPosition = self.mainButtonNode.textNode.position transition.updatePosition(node: self.mainButtonNode.textNode, position: CGPoint(x: inputTransition.menuButtonNode.frame.width / 2.0, y: inputTransition.menuButtonNode.frame.height / 2.0)) let sourceButtonCornerRadius = self.mainButtonNode.cornerRadius transition.updateCornerRadius(node: self.mainButtonNode, cornerRadius: inputTransition.menuButtonNode.cornerRadius) transition.updateSublayerTransformScale(node: self.mainButtonNode, scale: 0.2) self.mainButtonNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) let menuContentDelta = (sourceButtonFrame.width - inputTransition.menuButtonNode.frame.width) / 2.0 var menuIconSnapshotViewFrame = inputTransition.menuIconNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + menuContentDelta, dy: inputTransition.menuButtonNode.frame.minY) menuIconSnapshotViewFrame.origin.y = self.mainButtonNode.position.y - menuIconSnapshotViewFrame.height / 2.0 menuIconSnapshotView.frame = menuIconSnapshotViewFrame self.view.addSubview(menuIconSnapshotView) menuIconSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x - menuContentDelta, y: inputTransition.menuButtonNode.position.y)) var menuTextSnapshotViewFrame = inputTransition.menuTextNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + 19.0 + menuContentDelta, dy: inputTransition.menuButtonNode.frame.minY) menuTextSnapshotViewFrame.origin.y = self.mainButtonNode.position.y - menuTextSnapshotViewFrame.height / 2.0 menuTextSnapshotView.frame = menuTextSnapshotViewFrame self.view.addSubview(menuTextSnapshotView) menuTextSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x - menuContentDelta, y: inputTransition.menuButtonNode.position.y)) inputNodeSnapshotView.clipsToBounds = true inputNodeSnapshotView.contentMode = .right let targetInputFrame = CGRect(x: inputTransition.menuButtonNode.frame.maxX, y: 0.0, width: inputNodeSnapshotView.frame.width - inputTransition.menuButtonNode.frame.maxX, height: inputNodeSnapshotView.frame.height) inputNodeSnapshotView.frame = targetInputFrame.offsetBy(dx: targetInputFrame.width, dy: self.mainButtonNode.position.y - inputNodeSnapshotView.frame.height / 2.0) self.view.addSubview(inputNodeSnapshotView) transition.updateFrame(layer: inputNodeSnapshotView.layer, frame: targetInputFrame, completion: { [weak inputNodeSnapshotView, weak menuIconSnapshotView, weak menuTextSnapshotView, weak self] _ in inputNodeSnapshotView?.removeFromSuperview() self?.animatingTransition = false if !dismissed { menuIconSnapshotView?.removeFromSuperview() menuTextSnapshotView?.removeFromSuperview() self?.mainButtonNode.backgroundColor = sourceButtonColor self?.mainButtonNode.frame = sourceButtonFrame self?.mainButtonNode.textNode.position = sourceButtonTextPosition self?.mainButtonNode.textNode.layer.removeAllAnimations() self?.mainButtonNode.cornerRadius = sourceButtonCornerRadius } }) } if dismissed { Queue.mainQueue().after(0.01, action) } else { action() } } func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = layout self.buttons = buttons self.elevateProgress = elevateProgress let isButtonVisibleUpdated = self._isButtonVisible != self.mainButtonState.isVisible self._isButtonVisible = self.mainButtonState.isVisible let isSelectingUpdated = self.isSelecting != isSelecting self.isSelecting = isSelecting self.scrollNode.isUserInteractionEnabled = !isSelecting let isButtonVisible = self.mainButtonState.isVisible let isNarrowButton = isButtonVisible && self.mainButtonState.font == .regular var insets = layout.insets(options: []) if let inputHeight = layout.inputHeight, inputHeight > 0.0 && (isSelecting || isButtonVisible) { insets.bottom = inputHeight } else if layout.intrinsicInsets.bottom > 0.0 { insets.bottom = layout.intrinsicInsets.bottom } if isSelecting { self.loadTextNodeIfNeeded() } else { self.textInputPanelNode?.ensureUnfocused() } var textPanelHeight: CGFloat = 0.0 if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.isUserInteractionEnabled = isSelecting var panelTransition = transition if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { textInputPanelNode.frame = panelFrame } transition.updateFrame(node: textInputPanelNode, frame: panelFrame) if panelFrame.height > 0.0 { textPanelHeight = panelFrame.height } else { textPanelHeight = 45.0 } } let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: buttonSize.height + insets.bottom)) var containerTransition: ContainedViewLayoutTransition let containerFrame: CGRect if isButtonVisible { var height: CGFloat if layout.intrinsicInsets.bottom > 0.0 && (layout.inputHeight ?? 0.0).isZero { height = bounds.height if case .regular = layout.metrics.widthClass { if self.isStandalone { height -= 3.0 } else { height += 6.0 } } } else { height = bounds.height + 8.0 } if !isNarrowButton { height += 9.0 } containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: height)) } else if isSelecting { containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom)) } else { containerFrame = bounds } let containerBounds = CGRect(origin: CGPoint(), size: containerFrame.size) if isSelectingUpdated || isButtonVisibleUpdated { containerTransition = .animated(duration: 0.25, curve: .easeInOut) } else { containerTransition = transition } containerTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting || isButtonVisible ? 0.0 : 1.0) containerTransition.updateTransformScale(node: self.scrollNode, scale: isSelecting || isButtonVisible ? 0.85 : 1.0) if isSelectingUpdated { if isSelecting { self.loadTextNodeIfNeeded() if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.alpha = 1.0 textInputPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) textInputPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: CGPoint(), duration: 0.25, additive: true) } } else { if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.alpha = 0.0 textInputPanelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) textInputPanelNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 44.0), duration: 0.25, additive: true) } } } if self.containerNode.frame.size.width.isZero { containerTransition = .immediate } containerTransition.updateFrame(node: self.containerNode, frame: containerFrame) containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds) self.backgroundNode.update(size: containerBounds.size, transition: transition) containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel))) let _ = self.updateScrollLayoutIfNeeded(force: isSelectingUpdated || isButtonVisibleUpdated, transition: containerTransition) self.updateViews(transition: .immediate) if let progress = self.loadingProgress { let loadingProgressNode: LoadingProgressNode if let current = self.progressNode { loadingProgressNode = current } else { loadingProgressNode = LoadingProgressNode(color: self.presentationData.theme.rootController.tabBar.selectedIconColor) self.addSubnode(loadingProgressNode) self.progressNode = loadingProgressNode } let loadingProgressHeight: CGFloat = 2.0 let loadingProgressY: CGFloat = elevateProgress ? -loadingProgressHeight : -loadingProgressHeight / 2.0 transition.updateFrame(node: loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: loadingProgressY), size: CGSize(width: layout.size.width, height: loadingProgressHeight))) loadingProgressNode.updateProgress(progress, animated: true) } else if let progressNode = self.progressNode { self.progressNode = nil progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode] _ in progressNode?.removeFromSupernode() }) } let sideInset: CGFloat = 16.0 let buttonSize = CGSize(width: layout.size.width - (sideInset + layout.safeInsets.left) * 2.0, height: 50.0) let buttonTopInset: CGFloat = isNarrowButton ? 2.0 : 8.0 if !self.dismissed { self.mainButtonNode.updateLayout(size: buttonSize, state: self.mainButtonState, transition: transition) } if !self.animatingTransition { transition.updateFrame(node: self.mainButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + sideInset, y: isButtonVisible || self.fromMenu ? buttonTopInset : containerFrame.height), size: buttonSize)) } return containerFrame.height } func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateViews(transition: .immediate) } }