Swiftgram/submodules/AttachmentUI/Sources/AttachmentPanel.swift
2022-02-13 04:11:04 +03:00

927 lines
41 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import AttachmentTextInputPanelNode
import ChatPresentationInterfaceState
import ChatSendMessageActionUI
import ChatTextLinkEditUI
let panelButtonSize = CGSize(width: 80.0, height: 72.0)
let smallPanelButtonSize = CGSize(width: 60.0, height: 49.0)
private let iconSize = CGSize(width: 54.0, height: 42.0)
private let normalSideInset: CGFloat = 3.0
private let smallSideInset: CGFloat = 0.0
private enum AttachmentButtonTransition {
case transitionIn
case selection
}
private func generateBackgroundImage(colors: [UIColor]) -> UIImage? {
return generateImage(iconSize, rotatedContext: { size, context in
var locations: [CGFloat]
if colors.count == 3 {
locations = [1.0, 0.5, 0.0]
} else {
locations = [1.0, 0.0]
}
let colors: [CGColor] = colors.map { $0.cgColor }
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
if colors.count == 2 {
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: .drawsAfterEndLocation)
} else if colors.count == 3 {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: size.width, y: 0.0), options: .drawsAfterEndLocation)
}
// let center = CGPoint(x: 10.0, y: 10.0)
// context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation)
})
}
private let buttonGlowImage: UIImage? = {
let inset: CGFloat = 6.0
return generateImage(CGSize(width: iconSize.width + inset * 2.0, height: iconSize.height + inset * 2.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let rect = bounds.insetBy(dx: inset, dy: inset)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 21.0).cgPath
context.addRect(bounds)
context.addPath(path)
context.clip(using: .evenOdd)
context.addPath(path)
context.setShadow(offset: CGSize(), blur: 14.0, color: UIColor(rgb: 0xffffff, alpha: 0.8).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate)
}()
private let buttonSelectionMaskImage: UIImage? = {
let inset: CGFloat = 3.0
return generateImage(CGSize(width: iconSize.width + inset * 2.0, height: iconSize.height + inset * 2.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 23.0).cgPath
context.addPath(path)
context.setFillColor(UIColor(rgb: 0xffffff).cgColor)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate)
}()
private final class AttachButtonComponent: CombinedComponent {
let context: AccountContext
let type: AttachmentButtonType
let isSelected: Bool
let isCollapsed: Bool
let transitionFraction: CGFloat
let strings: PresentationStrings
let theme: PresentationTheme
let action: () -> Void
init(
context: AccountContext,
type: AttachmentButtonType,
isSelected: Bool,
isCollapsed: Bool,
transitionFraction: CGFloat,
strings: PresentationStrings,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.context = context
self.type = type
self.isSelected = isSelected
self.isCollapsed = isCollapsed
self.transitionFraction = transitionFraction
self.strings = strings
self.theme = theme
self.action = action
}
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.isCollapsed != rhs.isCollapsed {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
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(AttachButtonIconComponent.self)
let title = Child(Text.self)
return { context in
let name: String
let animationName: String?
let imageName: String?
let backgroundColors: [UIColor]
let foregroundColor: UIColor = .white
let isCollapsed = context.component.isCollapsed
switch context.component.type {
case .camera:
name = context.component.strings.Attachment_Camera
animationName = "anim_camera"
imageName = "Chat/Attach Menu/Camera"
backgroundColors = [UIColor(rgb: 0xba4aae), UIColor(rgb: 0xdd4e6f), UIColor(rgb: 0xf4b76c)]
case .gallery:
name = context.component.strings.Attachment_Gallery
animationName = "anim_gallery"
imageName = "Chat/Attach Menu/Gallery"
backgroundColors = [UIColor(rgb: 0x2071f1), UIColor(rgb: 0x1bc9fa)]
case .file:
name = context.component.strings.Attachment_File
animationName = "anim_file"
imageName = "Chat/Attach Menu/File"
backgroundColors = [UIColor(rgb: 0xed705d), UIColor(rgb: 0xffa14c)]
case .location:
name = context.component.strings.Attachment_Location
animationName = "anim_location"
imageName = "Chat/Attach Menu/Location"
backgroundColors = [UIColor(rgb: 0x5fb84f), UIColor(rgb: 0x99de6f)]
case .contact:
name = context.component.strings.Attachment_Contact
animationName = "anim_contact"
imageName = "Chat/Attach Menu/Contact"
backgroundColors = [UIColor(rgb: 0xaa47d6), UIColor(rgb: 0xd67cf4)]
case .poll:
name = context.component.strings.Attachment_Poll
animationName = "anim_poll"
imageName = "Chat/Attach Menu/Poll"
backgroundColors = [UIColor(rgb: 0xe9484f), UIColor(rgb: 0xee707e)]
case let .app(appName):
name = appName
animationName = nil
imageName = nil
backgroundColors = [UIColor(rgb: 0x000000), UIColor(rgb: 0x000000)]
}
let icon = icon.update(
component: AttachButtonIconComponent(
animationName: animationName,
imageName: imageName,
isSelected: context.component.isSelected,
backgroundColors: backgroundColors,
foregroundColor: foregroundColor,
theme: context.component.theme,
context: context.component.context,
action: context.component.action
),
availableSize: iconSize,
transition: context.transition
)
let title = title.update(
component: Text(
text: name,
font: Font.regular(11.0),
color: context.component.theme.actionSheet.primaryTextColor
),
availableSize: context.availableSize,
transition: .immediate
)
let topInset: CGFloat = 8.0
let spacing: CGFloat = 3.0 + UIScreenPixel
let normalIconScale = isCollapsed ? 0.7 : 1.0
let smallIconScale = isCollapsed ? 0.5 : 0.6
let iconScale = normalIconScale - (normalIconScale - smallIconScale) * abs(context.component.transitionFraction)
let iconOffset: CGFloat = (isCollapsed ? 10.0 : 20.0) * context.component.transitionFraction
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - icon.size.width) / 2.0) + iconOffset, y: isCollapsed ? 3.0 : topInset), size: icon.size)
var titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - title.size.width) / 2.0) + iconOffset, y: iconFrame.midY + (iconFrame.height * 0.5 * iconScale) + spacing), size: title.size)
if isCollapsed {
titleFrame.origin.y = floorToScreenPixels(iconFrame.midY - title.size.height / 2.0)
}
context.add(title
.position(CGPoint(x: titleFrame.midX, y: titleFrame.midY))
.opacity(isCollapsed ? 0.0 : 1.0 - abs(context.component.transitionFraction))
)
context.add(icon
.position(CGPoint(x: iconFrame.midX, y: iconFrame.midY))
.scale(iconScale)
)
return context.availableSize
}
}
}
private final class AttachButtonIconComponent: Component {
let animationName: String?
let imageName: String?
let isSelected: Bool
let backgroundColors: [UIColor]
let foregroundColor: UIColor
let theme: PresentationTheme
let context: AccountContext
let action: () -> Void
init(
animationName: String?,
imageName: String?,
isSelected: Bool,
backgroundColors: [UIColor],
foregroundColor: UIColor,
theme: PresentationTheme,
context: AccountContext,
action: @escaping () -> Void
) {
self.animationName = animationName
self.imageName = imageName
self.isSelected = isSelected
self.backgroundColors = backgroundColors
self.foregroundColor = foregroundColor
self.theme = theme
self.context = context
self.action = action
}
static func ==(lhs: AttachButtonIconComponent, rhs: AttachButtonIconComponent) -> Bool {
if lhs.animationName != rhs.animationName {
return false
}
if lhs.imageName != rhs.imageName {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.backgroundColors != rhs.backgroundColors {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.context !== rhs.context {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let containerView: UIView
private let glowView: UIImageView
private let selectionView: UIImageView
private let backgroundView: UIView
private let iconView: UIImageView
private let highlightView: UIView
private var action: (() -> Void)?
private var currentColors: [UIColor] = []
private var currentImageName: String?
private var currentIsSelected: Bool?
private let hapticFeedback = HapticFeedback()
init() {
self.containerView = UIView()
self.containerView.isUserInteractionEnabled = false
self.glowView = UIImageView()
self.glowView.image = buttonGlowImage
self.glowView.isUserInteractionEnabled = false
self.selectionView = UIImageView()
self.selectionView.image = buttonSelectionMaskImage
self.selectionView.isUserInteractionEnabled = false
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.backgroundView.isUserInteractionEnabled = false
self.backgroundView.layer.cornerRadius = 21.0
self.iconView = UIImageView()
self.highlightView = UIView()
self.highlightView.alpha = 0.0
self.highlightView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.1)
self.highlightView.isUserInteractionEnabled = false
super.init(frame: CGRect())
self.addSubview(self.containerView)
self.containerView.addSubview(self.glowView)
self.containerView.addSubview(self.selectionView)
self.containerView.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.iconView)
self.backgroundView.addSubview(self.highlightView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.containerView.layer.animateScale(from: 1.0, to: 0.9, duration: 0.3, removeOnCompletion: false)
strongSelf.highlightView.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightView.alpha = 1.0
strongSelf.hapticFeedback.impact(.click05)
} else {
if let presentationLayer = strongSelf.containerView.layer.presentation() {
strongSelf.containerView.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.2, removeOnCompletion: false)
}
strongSelf.highlightView.alpha = 0.0
strongSelf.highlightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
strongSelf.hapticFeedback.impact(.click06)
}
}
}
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action?()
}
func update(component: AttachButtonIconComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.action = component.action
if self.currentColors != component.backgroundColors {
self.currentColors = component.backgroundColors
self.backgroundView.layer.contents = generateBackgroundImage(colors: component.backgroundColors)?.cgImage
if let color = component.backgroundColors.last {
self.glowView.tintColor = color
self.selectionView.tintColor = color.withAlphaComponent(0.2)
}
}
if self.currentImageName != component.imageName {
self.currentImageName = component.imageName
if let imageName = component.imageName, let image = UIImage(bundleImageName: imageName) {
self.iconView.image = image
let scale: CGFloat = 0.875
let iconSize = CGSize(width: floorToScreenPixels(image.size.width * scale), height: floorToScreenPixels(image.size.height * scale))
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - iconSize.height) / 2.0)), size: iconSize)
}
}
if self.currentIsSelected != component.isSelected {
self.currentIsSelected = component.isSelected
transition.setScale(view: self.selectionView, scale: component.isSelected ? 1.0 : 0.8)
}
let contentFrame = CGRect(origin: CGPoint(), size: availableSize)
self.containerView.frame = contentFrame
self.backgroundView.frame = contentFrame
self.highlightView.frame = contentFrame
self.glowView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: contentFrame.width + 12.0, height: contentFrame.height + 12.0))
self.glowView.center = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
self.selectionView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: contentFrame.width + 6.0, height: contentFrame.height + 6.0))
self.selectionView.center = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
private let containerNode: ASDisplayNode
private var effectView: UIVisualEffectView?
private let scrollNode: ASScrollNode
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
private var textInputPanelNode: AttachmentTextInputPanelNode?
private var buttons: [AttachmentButtonType] = []
private var selectedIndex: Int = 1
private(set) var isCollapsed: Bool = false
private(set) var isSelecting: Bool = false
private var validLayout: ContainerViewLayout?
private var scrollLayout: (width: CGFloat, contentSize: CGSize)?
var selectionChanged: (AttachmentButtonType, Bool) -> 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 }
init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
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(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.scrollNode = ASScrollNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.separatorNode)
self.containerNode.addSubnode(self.scrollNode)
self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ 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
}, 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
}, sendBotStart: { _ in
}, botSwitchChatWithPayload: { _, _ in
}, beginMediaRecording: { _ in
}, finishMediaRecording: { _ in
}, stopMediaRecording: {
}, lockMediaRecording: {
}, 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
}, 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<Int>?
var text: String?
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)).string
}
inputMode = state.inputMode
return state
})
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text ?? "", link: nil, 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 else {
return
}
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, completion: {
}, sendMessage: { [weak textInputPanelNode] silently in
textInputPanelNode?.sendMessage(silently ? .silent : .generic)
}, schedule: { [weak textInputPanelNode] in
textInputPanelNode?.sendMessage(.schedule)
})
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
}, statuses: nil)
}
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
let effect: UIVisualEffect
switch self.presentationData.theme.actionSheet.backgroundType {
case .light:
effect = UIBlurEffect(style: .light)
case .dark:
effect = UIBlurEffect(style: .dark)
}
let effectView = UIVisualEffectView(effect: effect)
self.effectView = effectView
self.containerNode.view.insertSubview(effectView, at: 0)
}
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 updateViews(transition: Transition) {
guard let layout = self.validLayout else {
return
}
let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0)
let actualVisibleRect = self.scrollNode.bounds
var validButtons = Set<Int>()
let buttonSize = self.isCollapsed ? smallPanelButtonSize : panelButtonSize
var sideInset = self.isCollapsed ? smallSideInset : normalSideInset
let buttonsWidth = sideInset * 2.0 + buttonSize.width * CGFloat(self.buttons.count)
if buttonsWidth < layout.size.width {
sideInset = floorToScreenPixels((layout.size.width - buttonsWidth) / 2.0)
}
for i in 0 ..< self.buttons.count {
let buttonFrame = CGRect(origin: CGPoint(x: sideInset + buttonSize.width * CGFloat(i), y: 0.0), size: buttonSize)
if !visibleRect.intersects(buttonFrame) {
continue
}
validButtons.insert(i)
let edge = buttonSize.width * 0.75
let leftEdge = max(-edge, min(0.0, buttonFrame.minX - actualVisibleRect.minX)) / -edge
let rightEdge = min(edge, max(0.0, buttonFrame.maxX - actualVisibleRect.maxX)) / edge
let transitionFraction: CGFloat
if leftEdge > rightEdge {
transitionFraction = leftEdge
} else {
transitionFraction = -rightEdge
}
var buttonTransition = transition
let buttonView: ComponentHostView<Empty>
if let current = self.buttonViews[i] {
buttonView = current
} else {
buttonTransition = .immediate
buttonView = ComponentHostView<Empty>()
self.buttonViews[i] = buttonView
self.scrollNode.view.addSubview(buttonView)
}
let type = self.buttons[i]
let _ = buttonView.update(
transition: buttonTransition,
component: AnyComponent(AttachButtonComponent(
context: self.context,
type: type,
isSelected: i == self.selectedIndex,
isCollapsed: self.isCollapsed,
transitionFraction: transitionFraction,
strings: self.presentationData.strings,
theme: self.presentationData.theme,
action: { [weak self] in
if let strongSelf = self {
let ascending = i > strongSelf.selectedIndex
strongSelf.selectedIndex = i
strongSelf.selectionChanged(type, ascending)
strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring)))
}
})
),
environment: {},
containerSize: buttonSize
)
buttonTransition.setFrame(view: buttonView, frame: buttonFrame)
}
}
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
}
let buttonSize = self.isCollapsed ? smallPanelButtonSize : panelButtonSize
let contentSize = CGSize(width: (self.isCollapsed ? smallSideInset : normalSideInset) * 2.0 + CGFloat(self.buttons.count) * buttonSize.width, height: buttonSize.height)
self.scrollLayout = (layout.size.width, contentSize)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSelecting ? -panelButtonSize.height : 0.0), size: CGSize(width: layout.size.width, height: panelButtonSize.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, presentController: { [weak self] c in
if let strongSelf = self {
strongSelf.present(c)
}
})
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 update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isCollapsed: Bool, isSelecting: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
self.buttons = buttons
let isCollapsedUpdated = self.isCollapsed != isCollapsed
self.isCollapsed = isCollapsed
let isSelectingUpdated = self.isSelecting != isSelecting
self.isSelecting = isSelecting
self.scrollNode.isUserInteractionEnabled = !isSelecting
var insets = layout.insets(options: [])
if let inputHeight = layout.inputHeight, inputHeight > 0.0 && isSelecting {
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, rightInset: insets.right, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
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: panelButtonSize.height + insets.bottom))
let containerTransition: ContainedViewLayoutTransition
let containerFrame: CGRect
if isSelecting {
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom))
} else if isCollapsed {
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: smallPanelButtonSize.height + insets.bottom))
} else {
containerFrame = bounds
}
let containerBounds = CGRect(origin: CGPoint(), size: containerFrame.size)
if isCollapsedUpdated || isSelectingUpdated {
containerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
containerTransition = transition
}
containerTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting ? 0.0 : 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)
}
}
}
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel)))
if let effectView = self.effectView {
containerTransition.updateFrame(view: effectView, frame: bounds)
}
let _ = self.updateScrollLayoutIfNeeded(force: isCollapsedUpdated || isSelectingUpdated, transition: containerTransition)
var buttonTransition: Transition = .immediate
if isCollapsedUpdated {
buttonTransition = .easeInOut(duration: 0.25)
}
self.updateViews(transition: buttonTransition)
return containerFrame.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateViews(transition: .immediate)
}
}