2022-09-02 20:10:43 +04:00

576 lines
26 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import ComponentFlow
import TelegramPresentationData
import AccountContext
import ComponentDisplayAdapters
import MultilineTextComponent
import EmojiStatusComponent
protocol ContextMenuItemWithAction: AnyObject {
func performAction()
}
private final class ContextMenuActionItem: Component, ContextMenuItemWithAction {
typealias EnvironmentType = ContextMenuActionItemEnvironment
let title: String
let action: () -> Void
init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
static func ==(lhs: ContextMenuActionItem, rhs: ContextMenuActionItem) -> Bool {
if lhs.title != rhs.title {
return false
}
return true
}
func performAction() {
self.action()
}
final class View: UIView {
private let titleView: ComponentView<Empty>
override init(frame: CGRect) {
self.titleView = ComponentView<Empty>()
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ContextMenuActionItem, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let contextEnvironment = environment[EnvironmentType.self].value
let sideInset: CGFloat = 16.0
let height: CGFloat = 44.0
let titleSize = self.titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: contextEnvironment.theme.contextMenu.primaryColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
if let view = self.titleView.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: titleFrame)
}
return CGSize(width: sideInset * 2.0 + titleSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ContextMenuActionItemEnvironment: Equatable {
let theme: PresentationTheme
init(
theme: PresentationTheme
) {
self.theme = theme
}
static func ==(lhs: ContextMenuActionItemEnvironment, rhs: ContextMenuActionItemEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
return true
}
}
private final class ContextMenuActionsComponent: Component {
let theme: PresentationTheme
let items: [AnyComponentWithIdentity<ContextMenuActionItemEnvironment>]
init(
theme: PresentationTheme,
items: [AnyComponentWithIdentity<ContextMenuActionItemEnvironment>]
) {
self.theme = theme
self.items = items
}
static func ==(lhs: ContextMenuActionsComponent, rhs: ContextMenuActionsComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
final class View: UIButton {
private final class ItemView {
let view = ComponentView<ContextMenuActionItemEnvironment>()
let separatorView = UIView()
}
private let backgroundView: BlurredBackgroundView
private var itemViews: [AnyHashable: ItemView] = [:]
private var highligntedBackgroundView: UIView?
private var component: ContextMenuActionsComponent?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 14.0
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.setHighlightedItem(id: self.itemAtPoint(point: touch.location(in: self)))
return super.beginTracking(touch, with: event)
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.setHighlightedItem(id: self.itemAtPoint(point: touch.location(in: self)))
return super.continueTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
if let component = self.component, let touch = touch, let id = self.itemAtPoint(point: touch.location(in: self)) {
self.setHighlightedItem(id: id)
for item in component.items {
if item.id == id {
if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction {
itemComponent.performAction()
}
break
}
}
} else {
self.setHighlightedItem(id: nil)
}
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
self.setHighlightedItem(id: nil)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
self.setHighlightedItem(id: nil)
super.touchesCancelled(touches, with: event)
}
private func itemAtPoint(point: CGPoint) -> AnyHashable? {
for (id, itemView) in self.itemViews {
guard let itemComponentView = itemView.view.view else {
continue
}
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemComponentView.frame.minY), size: CGSize(width: self.bounds.width, height: itemComponentView.bounds.height))
if itemFrame.contains(point) {
return id
}
}
return nil
}
private func setHighlightedItem(id: AnyHashable?) {
if let component = self.component, let id = id, let itemView = self.itemViews[id], let itemComponentView = itemView.view.view {
let highligntedBackgroundView: UIView
if let current = self.highligntedBackgroundView {
highligntedBackgroundView = current
} else {
highligntedBackgroundView = UIView()
self.highligntedBackgroundView = highligntedBackgroundView
var found = false
outer: for subview in self.subviews {
for (_, listItemView) in self.itemViews {
if subview === listItemView.view.view {
self.insertSubview(highligntedBackgroundView, belowSubview: subview)
found = true
break outer
}
}
}
if !found {
self.insertSubview(highligntedBackgroundView, aboveSubview: self.backgroundView)
}
highligntedBackgroundView.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor
}
var highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: itemComponentView.frame.minY), size: CGSize(width: self.bounds.width, height: itemComponentView.bounds.height))
if id != component.items.last?.id {
highlightFrame.size.height += UIScreenPixel
}
highligntedBackgroundView.frame = highlightFrame
} else {
if let highligntedBackgroundView = self.highligntedBackgroundView {
self.highligntedBackgroundView = nil
highligntedBackgroundView.removeFromSuperview()
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
return self
}
func update(component: ContextMenuActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let availableItemSize = availableSize
var itemsSize = CGSize()
var validIds = Set<AnyHashable>()
var currentItems: [(id: AnyHashable, itemFrame: CGRect, itemTransition: Transition)] = []
for i in 0 ..< component.items.count {
let item = component.items[i]
validIds.insert(item.id)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView()
self.itemViews[item.id] = itemView
self.insertSubview(itemView.separatorView, aboveSubview: self.backgroundView)
}
let itemSize = itemView.view.update(
transition: itemTransition,
component: item.component,
environment: {
ContextMenuActionItemEnvironment(theme: component.theme)
},
containerSize: availableItemSize
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsSize.height), size: itemSize)
if let view = itemView.view.view {
if view.superview == nil {
self.addSubview(view)
}
itemTransition.setFrame(view: view, frame: itemFrame)
}
currentItems.append((item.id, itemFrame, itemTransition))
itemsSize.width = max(itemsSize.width, itemSize.width)
itemsSize.height += itemSize.height
}
itemsSize.width = max(itemsSize.width, 180.0)
for i in 0 ..< currentItems.count {
let item = currentItems[i]
guard let itemView = self.itemViews[item.id] else {
continue
}
itemView.separatorView.backgroundColor = component.theme.contextMenu.itemSeparatorColor
itemView.separatorView.isHidden = i == currentItems.count - 1
item.itemTransition.setFrame(view: itemView.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: item.itemFrame.maxY), size: CGSize(width: itemsSize.width, height: UIScreenPixel)))
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.view.view?.removeFromSuperview()
itemView.separatorView.removeFromSuperview()
}
}
self.backgroundView.updateColor(color: component.theme.contextMenu.backgroundColor, transition: .immediate)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: itemsSize))
self.backgroundView.update(size: itemsSize, transition: transition.containedViewLayoutTransition)
return itemsSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class EmojiStatusPreviewScreenComponent: Component {
struct StatusResult {
let timeout: Int
let sourceView: UIView
}
final class TransitionAnimation {
enum TransitionType {
case animateIn(sourceLayer: CALayer)
}
let transitionType: TransitionType
init(transitionType: TransitionType) {
self.transitionType = transitionType
}
}
typealias EnvironmentType = Empty
let theme: PresentationTheme
let strings: PresentationStrings
let item: EmojiStatusComponent
let dismiss: (StatusResult?) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
item: EmojiStatusComponent,
dismiss: @escaping (StatusResult?) -> Void
) {
self.theme = theme
self.strings = strings
self.item = item
self.dismiss = dismiss
}
static func ==(lhs: EmojiStatusPreviewScreenComponent, rhs: EmojiStatusPreviewScreenComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.item != rhs.item {
return false
}
return true
}
final class View: UIView {
private let backgroundView: BlurredBackgroundView
private let itemView: ComponentView<Empty>
private let actionsView: ComponentView<Empty>
private var component: EmojiStatusPreviewScreenComponent?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.itemView = ComponentView<Empty>()
self.actionsView = ComponentView<Empty>()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.component?.dismiss(nil)
}
}
func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
let itemSpacing: CGFloat = 12.0
let itemSize = self.itemView.update(
transition: transition,
component: AnyComponent(component.item),
environment: {},
containerSize: CGSize(width: 128.0, height: 128.0)
)
var menuItems: [AnyComponentWithIdentity<ContextMenuActionItemEnvironment>] = []
let delayDurations: [Int] = [
1 * 60 * 60,
2 * 60 * 60,
8 * 60 * 60,
2 * 24 * 60 * 60
]
for duration in delayDurations {
menuItems.append(AnyComponentWithIdentity(id: duration, component: AnyComponent(ContextMenuActionItem(
title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)),
action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
guard let itemComponentView = strongSelf.itemView.view else {
return
}
component.dismiss(StatusResult(timeout: duration, sourceView: itemComponentView))
}
))))
}
let actionsSize = self.actionsView.update(
transition: transition,
component: AnyComponent(ContextMenuActionsComponent(
theme: component.theme,
items: menuItems
)),
environment: {},
containerSize: availableSize
)
let totalContentHeight = itemSize.height + itemSpacing + actionsSize.height
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - totalContentHeight) / 2.0)), size: CGSize(width: availableSize.width, height: totalContentHeight))
let itemFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - itemSize.width) / 2.0), y: contentFrame.minY), size: itemSize)
let actionsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionsSize.width) / 2.0), y: itemFrame.maxY + itemSpacing), size: actionsSize)
if let itemComponentView = self.itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
transition.setFrame(view: itemComponentView, frame: itemFrame)
}
if let actionsComponentView = self.actionsView.view {
if actionsComponentView.superview == nil {
self.addSubview(actionsComponentView)
}
transition.setFrame(view: actionsComponentView, frame: actionsFrame)
}
self.backgroundView.updateColor(color: component.theme.contextMenu.dimColor, transition: .immediate)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
if let transitionAnimation = transition.userData(TransitionAnimation.self) {
switch transitionAnimation.transitionType {
case let .animateIn(sourceLayer):
var additionalPositionDifference = CGPoint()
if let copyLayer = sourceLayer.snapshotContentTree(), let itemComponentView = self.itemView.view {
sourceLayer.isHidden = true
copyLayer.frame = sourceLayer.convert(sourceLayer.bounds, to: self.layer)
self.layer.addSublayer(copyLayer)
copyLayer.animatePosition(from: copyLayer.frame.center, to: itemComponentView.frame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
copyLayer.animateScale(from: 1.0, to: itemComponentView.bounds.width / copyLayer.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
copyLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyLayer] _ in
copyLayer?.removeFromSuperlayer()
})
additionalPositionDifference = CGPoint(x: itemComponentView.frame.center.x - copyLayer.frame.center.x, y: itemComponentView.frame.center.y - copyLayer.frame.center.y)
itemComponentView.layer.animatePosition(from: copyLayer.frame.center, to: itemComponentView.frame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
itemComponentView.layer.animateScale(from: copyLayer.bounds.width / itemComponentView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let actionsComponentView = self.actionsView.view {
actionsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
actionsComponentView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6)
actionsComponentView.layer.animateSpring(from: (-actionsComponentView.bounds.height / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", duration: 0.6)
let _ = additionalPositionDifference
//actionsComponentView.layer.animatePosition(from: CGPoint(x: -additionalPositionDifference.x, y: -additionalPositionDifference.y), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
}
return availableSize
}
func animateOut(targetLayer: CALayer?, completion: @escaping () -> Void) {
if let targetLayer = targetLayer, let itemComponentView = self.itemView.view {
targetLayer.isHidden = false
let targetLayerPosition = targetLayer.position
let targetLayerSuperlayer = targetLayer.superlayer
var targetLayerIndexPosition: UInt32?
if let targetLayerSuperlayer = targetLayerSuperlayer {
if let index = targetLayerSuperlayer.sublayers?.firstIndex(of: targetLayer) {
targetLayerIndexPosition = UInt32(index)
}
}
let localTargetPosition = targetLayer.convert(targetLayer.bounds.center, to: self.layer)
self.layer.addSublayer(targetLayer)
targetLayer.position = localTargetPosition
targetLayer.animatePosition(from: itemComponentView.frame.center, to: localTargetPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
targetLayer.animateScale(from: itemComponentView.bounds.width / targetLayer.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak targetLayer, weak targetLayerSuperlayer] _ in
if let targetLayer = targetLayer, let targetLayerSuperlayer = targetLayerSuperlayer {
if let targetLayerIndexPosition = targetLayerIndexPosition {
targetLayerSuperlayer.insertSublayer(targetLayer, at: targetLayerIndexPosition)
targetLayer.position = targetLayerPosition
}
}
completion()
})
targetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
itemComponentView.layer.animatePosition(from: itemComponentView.frame.center, to: localTargetPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
itemComponentView.layer.animateScale(from: 1.0, to: targetLayer.bounds.width / itemComponentView.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let actionsComponentView = self.actionsView.view {
actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
})
}
} else {
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let actionsComponentView = self.actionsView.view {
actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
} else {
completion()
}
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}