mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-01 10:23:15 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
d6207366d2
@ -502,7 +502,11 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|
|||||||
})
|
})
|
||||||
|> mapToSignal { coordinate -> Signal<PeersNearbyData?, NoError> in
|
|> mapToSignal { coordinate -> Signal<PeersNearbyData?, NoError> in
|
||||||
guard let coordinate = coordinate else {
|
guard let coordinate = coordinate else {
|
||||||
let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil)
|
#if !DEBUG
|
||||||
|
#error("fix")
|
||||||
|
#endif
|
||||||
|
preconditionFailure()
|
||||||
|
/*let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil)
|
||||||
return peersNearbyContext.get()
|
return peersNearbyContext.get()
|
||||||
|> map { peersNearby -> PeersNearbyData in
|
|> map { peersNearby -> PeersNearbyData in
|
||||||
var isVisible = false
|
var isVisible = false
|
||||||
@ -515,7 +519,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: [])
|
return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: [])
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
|
@ -369,6 +369,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
|
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
|
||||||
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
|
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
|
||||||
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
||||||
|
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
|
@ -82,6 +82,8 @@ public final class RippleEffectView: MTKView {
|
|||||||
self.framebufferOnly = true
|
self.framebufferOnly = true
|
||||||
|
|
||||||
self.isPaused = false
|
self.isPaused = false
|
||||||
|
|
||||||
|
self.isUserInteractionEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||||
|
@ -28,8 +28,10 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
|
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
|
||||||
|
"//submodules/TelegramUI/Components/TokenListTextField",
|
||||||
"//submodules/AvatarNode",
|
"//submodules/AvatarNode",
|
||||||
"//submodules/CheckNode",
|
"//submodules/CheckNode",
|
||||||
|
"//submodules/PeerPresenceStatusManager",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -1,196 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import ComponentFlow
|
|
||||||
import MultilineTextComponent
|
|
||||||
import TelegramPresentationData
|
|
||||||
|
|
||||||
final class ActionListItemComponent: Component {
|
|
||||||
let theme: PresentationTheme
|
|
||||||
let sideInset: CGFloat
|
|
||||||
let iconName: String?
|
|
||||||
let title: String
|
|
||||||
let hasNext: Bool
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
init(
|
|
||||||
theme: PresentationTheme,
|
|
||||||
sideInset: CGFloat,
|
|
||||||
iconName: String?,
|
|
||||||
title: String,
|
|
||||||
hasNext: Bool,
|
|
||||||
action: @escaping () -> Void
|
|
||||||
) {
|
|
||||||
self.theme = theme
|
|
||||||
self.sideInset = sideInset
|
|
||||||
self.iconName = iconName
|
|
||||||
self.title = title
|
|
||||||
self.hasNext = hasNext
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool {
|
|
||||||
if lhs.theme !== rhs.theme {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.sideInset != rhs.sideInset {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.iconName != rhs.iconName {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.title != rhs.title {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.hasNext != rhs.hasNext {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
final class View: UIView {
|
|
||||||
private let containerButton: HighlightTrackingButton
|
|
||||||
|
|
||||||
private let title = ComponentView<Empty>()
|
|
||||||
private let iconView: UIImageView
|
|
||||||
private let separatorLayer: SimpleLayer
|
|
||||||
|
|
||||||
private var highlightBackgroundFrame: CGRect?
|
|
||||||
private var highlightBackgroundLayer: SimpleLayer?
|
|
||||||
|
|
||||||
private var component: ActionListItemComponent?
|
|
||||||
private weak var state: EmptyComponentState?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
self.separatorLayer = SimpleLayer()
|
|
||||||
|
|
||||||
self.containerButton = HighlightTrackingButton()
|
|
||||||
|
|
||||||
self.iconView = UIImageView()
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
self.layer.addSublayer(self.separatorLayer)
|
|
||||||
self.addSubview(self.containerButton)
|
|
||||||
|
|
||||||
self.containerButton.addSubview(self.iconView)
|
|
||||||
|
|
||||||
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
|
||||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isHighlighted {
|
|
||||||
self.superview?.bringSubviewToFront(self)
|
|
||||||
|
|
||||||
let highlightBackgroundLayer: SimpleLayer
|
|
||||||
if let current = self.highlightBackgroundLayer {
|
|
||||||
highlightBackgroundLayer = current
|
|
||||||
} else {
|
|
||||||
highlightBackgroundLayer = SimpleLayer()
|
|
||||||
self.highlightBackgroundLayer = highlightBackgroundLayer
|
|
||||||
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
|
|
||||||
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
|
|
||||||
}
|
|
||||||
highlightBackgroundLayer.frame = highlightBackgroundFrame
|
|
||||||
highlightBackgroundLayer.opacity = 1.0
|
|
||||||
} else {
|
|
||||||
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
|
|
||||||
self.highlightBackgroundLayer = nil
|
|
||||||
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
|
|
||||||
highlightBackgroundLayer?.removeFromSuperlayer()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func pressed() {
|
|
||||||
guard let component = self.component else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
component.action()
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
||||||
let themeUpdated = self.component?.theme !== component.theme
|
|
||||||
|
|
||||||
if self.component?.iconName != component.iconName {
|
|
||||||
if let iconName = component.iconName {
|
|
||||||
self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate)
|
|
||||||
} else {
|
|
||||||
self.iconView.image = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if themeUpdated {
|
|
||||||
self.iconView.tintColor = component.theme.list.itemAccentColor
|
|
||||||
}
|
|
||||||
|
|
||||||
self.component = component
|
|
||||||
self.state = state
|
|
||||||
|
|
||||||
let contextInset: CGFloat = 0.0
|
|
||||||
|
|
||||||
let height: CGFloat = 44.0
|
|
||||||
let verticalInset: CGFloat = 1.0
|
|
||||||
let leftInset: CGFloat = 62.0 + component.sideInset
|
|
||||||
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
|
||||||
|
|
||||||
let previousTitleFrame = self.title.view?.frame
|
|
||||||
|
|
||||||
let titleSize = self.title.update(
|
|
||||||
transition: .immediate,
|
|
||||||
component: AnyComponent(MultilineTextComponent(
|
|
||||||
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor))
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
let centralContentHeight: CGFloat = titleSize.height
|
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
|
||||||
if let titleView = self.title.view {
|
|
||||||
if titleView.superview == nil {
|
|
||||||
titleView.isUserInteractionEnabled = false
|
|
||||||
self.containerButton.addSubview(titleView)
|
|
||||||
}
|
|
||||||
titleView.frame = titleFrame
|
|
||||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
|
||||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let iconImage = self.iconView.image {
|
|
||||||
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size))
|
|
||||||
}
|
|
||||||
|
|
||||||
if themeUpdated {
|
|
||||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
|
||||||
}
|
|
||||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
|
||||||
self.separatorLayer.isHidden = !component.hasNext
|
|
||||||
|
|
||||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
|
||||||
|
|
||||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
|
||||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
|
||||||
|
|
||||||
return CGSize(width: availableSize.width, height: height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,308 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import MultilineTextComponent
|
||||||
|
import AvatarNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
import CheckNode
|
||||||
|
|
||||||
|
final class CategoryListItemComponent: Component {
|
||||||
|
enum SelectionState: Equatable {
|
||||||
|
case none
|
||||||
|
case editing(isSelected: Bool, isTinted: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let title: String
|
||||||
|
let color: ShareWithPeersScreenComponent.CategoryColor
|
||||||
|
let iconName: String?
|
||||||
|
let subtitle: String?
|
||||||
|
let selectionState: SelectionState
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
title: String,
|
||||||
|
color: ShareWithPeersScreenComponent.CategoryColor,
|
||||||
|
iconName: String?,
|
||||||
|
subtitle: String?,
|
||||||
|
selectionState: SelectionState,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.title = title
|
||||||
|
self.color = color
|
||||||
|
self.iconName = iconName
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.selectionState = selectionState
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.color != rhs.color {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.iconName != rhs.iconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subtitle != rhs.subtitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.selectionState != rhs.selectionState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
private let iconView: UIImageView
|
||||||
|
|
||||||
|
private var checkLayer: CheckLayer?
|
||||||
|
|
||||||
|
private var component: CategoryListItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.containerButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
self.addSubview(self.containerButton)
|
||||||
|
self.containerButton.addSubview(self.iconView)
|
||||||
|
|
||||||
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
var hasSelectionUpdated = false
|
||||||
|
if let previousComponent = self.component {
|
||||||
|
switch previousComponent.selectionState {
|
||||||
|
case .none:
|
||||||
|
if case .none = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
case .editing:
|
||||||
|
if case .editing = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.component?.iconName != component.iconName || self.component?.color != component.color), let iconName = component.iconName {
|
||||||
|
let mappedColor: AvatarBackgroundColor
|
||||||
|
var iconScale: CGFloat = 1.0
|
||||||
|
switch component.color {
|
||||||
|
case .blue:
|
||||||
|
mappedColor = .blue
|
||||||
|
case .yellow:
|
||||||
|
mappedColor = .yellow
|
||||||
|
case .green:
|
||||||
|
mappedColor = .green
|
||||||
|
case .purple:
|
||||||
|
mappedColor = .purple
|
||||||
|
case .red:
|
||||||
|
mappedColor = .red
|
||||||
|
case .violet:
|
||||||
|
mappedColor = .violet
|
||||||
|
}
|
||||||
|
|
||||||
|
iconScale = 1.0
|
||||||
|
|
||||||
|
self.iconView.image = generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white), iconScale: iconScale, cornerRadius: 20.0, color: mappedColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contextInset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let height: CGFloat = 60.0
|
||||||
|
let verticalInset: CGFloat = 1.0
|
||||||
|
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||||
|
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||||
|
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||||
|
|
||||||
|
if case let .editing(isSelected, isTinted) = component.selectionState {
|
||||||
|
leftInset += 44.0
|
||||||
|
avatarLeftInset += 44.0
|
||||||
|
let checkSize: CGFloat = 22.0
|
||||||
|
|
||||||
|
let checkLayer: CheckLayer
|
||||||
|
if let current = self.checkLayer {
|
||||||
|
checkLayer = current
|
||||||
|
if themeUpdated {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||||
|
}
|
||||||
|
checkLayer.theme = theme
|
||||||
|
}
|
||||||
|
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||||
|
} else {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||||
|
}
|
||||||
|
checkLayer = CheckLayer(theme: theme)
|
||||||
|
self.checkLayer = checkLayer
|
||||||
|
self.containerButton.layer.addSublayer(checkLayer)
|
||||||
|
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||||
|
checkLayer.setSelected(isSelected, animated: false)
|
||||||
|
checkLayer.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||||
|
} else {
|
||||||
|
if let checkLayer = self.checkLayer {
|
||||||
|
self.checkLayer = nil
|
||||||
|
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||||
|
checkLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize: CGFloat = 40.0
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.iconView, frame: avatarFrame)
|
||||||
|
|
||||||
|
let labelData: (String, Bool) = ("", false)
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let previousTitleFrame = self.title.view?.frame
|
||||||
|
var previousTitleContents: UIView?
|
||||||
|
if hasSelectionUpdated && !"".isEmpty {
|
||||||
|
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat = 1.0
|
||||||
|
var centralContentHeight: CGFloat = titleSize.height
|
||||||
|
if !labelData.0.isEmpty {
|
||||||
|
centralContentHeight += titleSpacing + labelSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||||
|
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||||
|
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||||
|
self.addSubview(previousTitleContents)
|
||||||
|
|
||||||
|
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||||
|
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||||
|
previousTitleContents?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import AvatarNode
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import CheckNode
|
import CheckNode
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
import PeerPresenceStatusManager
|
||||||
|
|
||||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ final class PeerListItemComponent: Component {
|
|||||||
let title: String
|
let title: String
|
||||||
let peer: EnginePeer?
|
let peer: EnginePeer?
|
||||||
let subtitle: String?
|
let subtitle: String?
|
||||||
|
let presence: EnginePeer.Presence?
|
||||||
let selectionState: SelectionState
|
let selectionState: SelectionState
|
||||||
let hasNext: Bool
|
let hasNext: Bool
|
||||||
let action: (EnginePeer) -> Void
|
let action: (EnginePeer) -> Void
|
||||||
@ -39,6 +41,7 @@ final class PeerListItemComponent: Component {
|
|||||||
title: String,
|
title: String,
|
||||||
peer: EnginePeer?,
|
peer: EnginePeer?,
|
||||||
subtitle: String?,
|
subtitle: String?,
|
||||||
|
presence: EnginePeer.Presence?,
|
||||||
selectionState: SelectionState,
|
selectionState: SelectionState,
|
||||||
hasNext: Bool,
|
hasNext: Bool,
|
||||||
action: @escaping (EnginePeer) -> Void
|
action: @escaping (EnginePeer) -> Void
|
||||||
@ -50,6 +53,7 @@ final class PeerListItemComponent: Component {
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
self.presence = presence
|
||||||
self.selectionState = selectionState
|
self.selectionState = selectionState
|
||||||
self.hasNext = hasNext
|
self.hasNext = hasNext
|
||||||
self.action = action
|
self.action = action
|
||||||
@ -77,6 +81,9 @@ final class PeerListItemComponent: Component {
|
|||||||
if lhs.subtitle != rhs.subtitle {
|
if lhs.subtitle != rhs.subtitle {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.presence != rhs.presence {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.selectionState != rhs.selectionState {
|
if lhs.selectionState != rhs.selectionState {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -99,6 +106,8 @@ final class PeerListItemComponent: Component {
|
|||||||
private var component: PeerListItemComponent?
|
private var component: PeerListItemComponent?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
private var presenceManager: PeerPresenceStatusManager?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.separatorLayer = SimpleLayer()
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
@ -128,6 +137,12 @@ final class PeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let animationHint = transition.userData(ShareWithPeersScreenComponent.AnimationHint.self)
|
||||||
|
var synchronousLoad = false
|
||||||
|
if let animationHint, animationHint.contentReloaded {
|
||||||
|
synchronousLoad = true
|
||||||
|
}
|
||||||
|
|
||||||
let themeUpdated = self.component?.theme !== component.theme
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
var hasSelectionUpdated = false
|
var hasSelectionUpdated = false
|
||||||
@ -146,6 +161,23 @@ final class PeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let presence = component.presence {
|
||||||
|
let presenceManager: PeerPresenceStatusManager
|
||||||
|
if let current = self.presenceManager {
|
||||||
|
presenceManager = current
|
||||||
|
} else {
|
||||||
|
presenceManager = PeerPresenceStatusManager(update: { [weak self] in
|
||||||
|
self?.state?.updated(transition: .immediate)
|
||||||
|
})
|
||||||
|
self.presenceManager = presenceManager
|
||||||
|
}
|
||||||
|
presenceManager.reset(presence: presence)
|
||||||
|
} else {
|
||||||
|
if self.presenceManager != nil {
|
||||||
|
self.presenceManager = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
@ -210,15 +242,15 @@ final class PeerListItemComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
clipStyle = .round
|
clipStyle = .round
|
||||||
}
|
}
|
||||||
if peer.id == component.context.account.peerId {
|
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
|
||||||
} else {
|
|
||||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let labelData: (String, Bool)
|
let labelData: (String, Bool)
|
||||||
if let subtitle = component.subtitle {
|
|
||||||
|
if let presence = component.presence {
|
||||||
|
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||||
|
labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(timestamp))
|
||||||
|
} else if let subtitle = component.subtitle {
|
||||||
labelData = (subtitle, false)
|
labelData = (subtitle, false)
|
||||||
} else if case .legacyGroup = component.peer {
|
} else if case .legacyGroup = component.peer {
|
||||||
labelData = (component.strings.Group_Status, false)
|
labelData = (component.strings.Group_Status, false)
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
import MultilineTextComponent
|
||||||
|
|
||||||
|
final class SectionHeaderComponent: Component {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let backgroundView: BlurredBackgroundView
|
||||||
|
|
||||||
|
private var component: SectionHeaderComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let height: CGFloat = 28.0
|
||||||
|
let leftInset: CGFloat = component.sideInset
|
||||||
|
let rightInset: CGFloat = component.sideInset
|
||||||
|
|
||||||
|
let previousTitleFrame = self.title.view?.frame
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||||
|
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = CGSize(width: availableSize.width, height: height)
|
||||||
|
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
25
submodules/TelegramUI/Components/TokenListTextField/BUILD
Normal file
25
submodules/TelegramUI/Components/TokenListTextField/BUILD
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "TokenListTextField",
|
||||||
|
module_name = "TokenListTextField",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
"//submodules/AvatarNode",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,534 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AvatarNode
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
struct EditableTokenListToken {
|
||||||
|
enum Subject {
|
||||||
|
case peer(EnginePeer)
|
||||||
|
case category(UIImage?)
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: AnyHashable
|
||||||
|
let title: String
|
||||||
|
let fixedPosition: Int?
|
||||||
|
let subject: Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee))
|
||||||
|
|
||||||
|
private func caretAnimation() -> CAAnimation {
|
||||||
|
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||||
|
animation.values = [1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber]
|
||||||
|
let firstDuration = 0.3
|
||||||
|
let secondDuration = 0.25
|
||||||
|
let restDuration = 0.35
|
||||||
|
let duration = firstDuration + secondDuration + restDuration
|
||||||
|
let keyTimes: [NSNumber] = [0.0 as NSNumber, (firstDuration / duration) as NSNumber, ((firstDuration + secondDuration) / duration) as NSNumber, ((firstDuration + secondDuration + restDuration) / duration) as NSNumber]
|
||||||
|
|
||||||
|
animation.keyTimes = keyTimes
|
||||||
|
animation.duration = duration
|
||||||
|
animation.repeatCount = Float.greatestFiniteMagnitude
|
||||||
|
return animation
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateRemoveIcon(_ color: UIColor) -> UIImage? {
|
||||||
|
return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
context.setStrokeColor(color.cgColor)
|
||||||
|
context.setLineWidth(2.0 - UIScreenPixel)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
|
||||||
|
let length: CGFloat = 8.0
|
||||||
|
context.move(to: CGPoint(x: 7.0, y: 7.0))
|
||||||
|
context.addLine(to: CGPoint(x: 7.0 + length, y: 7.0 + length))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 7.0 + length, y: 7.0))
|
||||||
|
context.addLine(to: CGPoint(x: 7.0, y: 7.0 + length))
|
||||||
|
context.strokePath()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EditableTokenListNodeTheme {
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let separatorColor: UIColor
|
||||||
|
let placeholderTextColor: UIColor
|
||||||
|
let primaryTextColor: UIColor
|
||||||
|
let tokenBackgroundColor: UIColor
|
||||||
|
let selectedTextColor: UIColor
|
||||||
|
let selectedBackgroundColor: UIColor
|
||||||
|
let accentColor: UIColor
|
||||||
|
let keyboardColor: PresentationThemeKeyboardColor
|
||||||
|
|
||||||
|
init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) {
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.separatorColor = separatorColor
|
||||||
|
self.placeholderTextColor = placeholderTextColor
|
||||||
|
self.primaryTextColor = primaryTextColor
|
||||||
|
self.tokenBackgroundColor = tokenBackgroundColor
|
||||||
|
self.selectedTextColor = selectedTextColor
|
||||||
|
self.selectedBackgroundColor = selectedBackgroundColor
|
||||||
|
self.accentColor = accentColor
|
||||||
|
self.keyboardColor = keyboardColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TokenNode: ASDisplayNode {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let presentationTheme: PresentationTheme
|
||||||
|
|
||||||
|
let theme: EditableTokenListNodeTheme
|
||||||
|
let token: EditableTokenListToken
|
||||||
|
let avatarNode: AvatarNode
|
||||||
|
let categoryAvatarNode: ASImageNode
|
||||||
|
let removeIconNode: ASImageNode
|
||||||
|
let titleNode: ASTextNode
|
||||||
|
let backgroundNode: ASImageNode
|
||||||
|
let selectedBackgroundNode: ASImageNode
|
||||||
|
var isSelected: Bool = false
|
||||||
|
// didSet {
|
||||||
|
// if self.isSelected != oldValue {
|
||||||
|
// self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||||
|
// self.titleNode.redrawIfPossible()
|
||||||
|
// self.backgroundNode.isHidden = self.isSelected
|
||||||
|
// self.selectedBackgroundNode.isHidden = !self.isSelected
|
||||||
|
//
|
||||||
|
// self.avatarNode.isHidden = self.isSelected
|
||||||
|
// self.categoryAvatarNode.isHidden = self.isSelected
|
||||||
|
// self.removeIconNode.isHidden = !self.isSelected
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, token: EditableTokenListToken, isSelected: Bool) {
|
||||||
|
self.context = context
|
||||||
|
self.presentationTheme = presentationTheme
|
||||||
|
self.theme = theme
|
||||||
|
self.token = token
|
||||||
|
self.titleNode = ASTextNode()
|
||||||
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
|
self.titleNode.displaysAsynchronously = false
|
||||||
|
self.titleNode.maximumNumberOfLines = 1
|
||||||
|
|
||||||
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
|
||||||
|
self.categoryAvatarNode = ASImageNode()
|
||||||
|
self.categoryAvatarNode.displaysAsynchronously = false
|
||||||
|
self.categoryAvatarNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
self.removeIconNode = ASImageNode()
|
||||||
|
self.removeIconNode.alpha = 0.0
|
||||||
|
self.removeIconNode.displaysAsynchronously = false
|
||||||
|
self.removeIconNode.displayWithoutProcessing = true
|
||||||
|
self.removeIconNode.image = generateRemoveIcon(theme.selectedTextColor)
|
||||||
|
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
switch token.subject {
|
||||||
|
case .peer:
|
||||||
|
cornerRadius = 24.0
|
||||||
|
case .category:
|
||||||
|
cornerRadius = 14.0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundNode = ASImageNode()
|
||||||
|
self.backgroundNode.displaysAsynchronously = false
|
||||||
|
self.backgroundNode.displayWithoutProcessing = true
|
||||||
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.tokenBackgroundColor)
|
||||||
|
|
||||||
|
self.selectedBackgroundNode = ASImageNode()
|
||||||
|
self.selectedBackgroundNode.alpha = 0.0
|
||||||
|
self.selectedBackgroundNode.displaysAsynchronously = false
|
||||||
|
self.selectedBackgroundNode.displayWithoutProcessing = true
|
||||||
|
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.selectedBackgroundColor)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.selectedBackgroundNode)
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.removeIconNode)
|
||||||
|
|
||||||
|
switch token.subject {
|
||||||
|
case let .peer(peer):
|
||||||
|
self.addSubnode(self.avatarNode)
|
||||||
|
self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer)
|
||||||
|
case let .category(image):
|
||||||
|
self.addSubnode(self.categoryAvatarNode)
|
||||||
|
self.categoryAvatarNode.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateIsSelected(isSelected, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||||
|
let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height))
|
||||||
|
return CGSize(width: 22.0 + titleSize.width + 16.0, height: 28.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
let titleSize = self.titleNode.calculatedSize
|
||||||
|
if titleSize.width.isZero {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.backgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
|
||||||
|
self.selectedBackgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
|
||||||
|
self.avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 22.0, height: 22.0))
|
||||||
|
self.categoryAvatarNode.frame = self.avatarNode.frame
|
||||||
|
self.removeIconNode.frame = self.avatarNode.frame
|
||||||
|
|
||||||
|
self.titleNode.frame = CGRect(origin: CGPoint(x: 29.0, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIsSelected(_ isSelected: Bool, animated: Bool) {
|
||||||
|
guard self.isSelected != isSelected else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isSelected = isSelected
|
||||||
|
|
||||||
|
self.avatarNode.alpha = isSelected ? 0.0 : 1.0
|
||||||
|
self.categoryAvatarNode.alpha = isSelected ? 0.0 : 1.0
|
||||||
|
self.removeIconNode.alpha = isSelected ? 1.0 : 0.0
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
if isSelected {
|
||||||
|
self.selectedBackgroundNode.alpha = 1.0
|
||||||
|
self.selectedBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.avatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
|
|
||||||
|
self.categoryAvatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.categoryAvatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
|
|
||||||
|
self.removeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.removeIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||||
|
} else {
|
||||||
|
self.selectedBackgroundNode.alpha = 0.0
|
||||||
|
self.selectedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.avatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.categoryAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.categoryAvatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.removeIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.removeIconNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let snapshotView = self.titleNode.view.snapshotContentTree() {
|
||||||
|
self.titleNode.view.superview?.addSubview(snapshotView)
|
||||||
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||||
|
snapshotView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||||
|
self.titleNode.redrawIfPossible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CaretIndicatorNode: ASImageNode {
|
||||||
|
override func willEnterHierarchy() {
|
||||||
|
super.willEnterHierarchy()
|
||||||
|
|
||||||
|
if self.layer.animation(forKey: "blink") == nil {
|
||||||
|
self.layer.add(caretAnimation(), forKey: "blink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let presentationTheme: PresentationTheme
|
||||||
|
|
||||||
|
private let theme: EditableTokenListNodeTheme
|
||||||
|
private let backgroundNode: NavigationBackgroundNode
|
||||||
|
private let scrollNode: ASScrollNode
|
||||||
|
private let placeholderNode: ASTextNode
|
||||||
|
private var tokenNodes: [TokenNode] = []
|
||||||
|
private let separatorNode: ASDisplayNode
|
||||||
|
private let textFieldScrollNode: ASScrollNode
|
||||||
|
private let textFieldNode: TextFieldNode
|
||||||
|
private let caretIndicatorNode: CaretIndicatorNode
|
||||||
|
private var selectedTokenId: AnyHashable?
|
||||||
|
|
||||||
|
var isFocused: Bool {
|
||||||
|
return self.textFieldNode.view.isFirstResponder
|
||||||
|
}
|
||||||
|
|
||||||
|
var textUpdated: ((String) -> Void)?
|
||||||
|
var deleteToken: ((AnyHashable) -> Void)?
|
||||||
|
var textReturned: (() -> Void)?
|
||||||
|
var isFirstResponderChanged: (() -> Void)?
|
||||||
|
|
||||||
|
init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) {
|
||||||
|
self.context = context
|
||||||
|
self.presentationTheme = presentationTheme
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor)
|
||||||
|
|
||||||
|
self.scrollNode = ASScrollNode()
|
||||||
|
self.scrollNode.view.alwaysBounceVertical = true
|
||||||
|
|
||||||
|
self.placeholderNode = ASTextNode()
|
||||||
|
self.placeholderNode.isUserInteractionEnabled = false
|
||||||
|
self.placeholderNode.maximumNumberOfLines = 1
|
||||||
|
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor)
|
||||||
|
|
||||||
|
self.textFieldScrollNode = ASScrollNode()
|
||||||
|
|
||||||
|
self.textFieldNode = TextFieldNode()
|
||||||
|
self.textFieldNode.textField.font = Font.regular(15.0)
|
||||||
|
self.textFieldNode.textField.textColor = theme.primaryTextColor
|
||||||
|
self.textFieldNode.textField.autocorrectionType = .no
|
||||||
|
self.textFieldNode.textField.returnKeyType = .done
|
||||||
|
self.textFieldNode.textField.keyboardAppearance = theme.keyboardColor.keyboardAppearance
|
||||||
|
self.textFieldNode.textField.tintColor = theme.accentColor
|
||||||
|
|
||||||
|
self.caretIndicatorNode = CaretIndicatorNode()
|
||||||
|
self.caretIndicatorNode.isLayerBacked = true
|
||||||
|
self.caretIndicatorNode.displayWithoutProcessing = true
|
||||||
|
self.caretIndicatorNode.displaysAsynchronously = false
|
||||||
|
self.caretIndicatorNode.image = caretIndicatorImage
|
||||||
|
|
||||||
|
self.separatorNode = ASDisplayNode()
|
||||||
|
self.separatorNode.isLayerBacked = true
|
||||||
|
self.separatorNode.backgroundColor = theme.separatorColor
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.scrollNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.separatorNode)
|
||||||
|
self.scrollNode.addSubnode(self.placeholderNode)
|
||||||
|
self.scrollNode.addSubnode(self.textFieldScrollNode)
|
||||||
|
self.textFieldScrollNode.addSubnode(self.textFieldNode)
|
||||||
|
//self.scrollNode.addSubnode(self.caretIndicatorNode)
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
self.textFieldNode.textField.delegate = self
|
||||||
|
self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||||
|
self.textFieldNode.textField.didDeleteBackwardWhileEmpty = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if let selectedTokenId = strongSelf.selectedTokenId {
|
||||||
|
strongSelf.deleteToken?(selectedTokenId)
|
||||||
|
strongSelf.updateSelectedTokenId(nil)
|
||||||
|
} else if let tokenNode = strongSelf.tokenNodes.last {
|
||||||
|
strongSelf.updateSelectedTokenId(tokenNode.token.id, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||||
|
let validTokens = Set<AnyHashable>(tokens.map { $0.id })
|
||||||
|
|
||||||
|
for i in (0 ..< self.tokenNodes.count).reversed() {
|
||||||
|
let tokenNode = tokenNodes[i]
|
||||||
|
if !validTokens.contains(tokenNode.token.id) {
|
||||||
|
self.tokenNodes.remove(at: i)
|
||||||
|
if case .immediate = transition {
|
||||||
|
tokenNode.removeFromSupernode()
|
||||||
|
} else {
|
||||||
|
tokenNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tokenNode] _ in
|
||||||
|
tokenNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
tokenNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedTokenId = self.selectedTokenId, !validTokens.contains(selectedTokenId) {
|
||||||
|
self.selectedTokenId = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 12.0 + leftInset
|
||||||
|
let verticalInset: CGFloat = 6.0
|
||||||
|
|
||||||
|
|
||||||
|
var animationDelay = 0.0
|
||||||
|
var currentOffset = CGPoint(x: sideInset, y: verticalInset)
|
||||||
|
for token in tokens {
|
||||||
|
var currentNode: TokenNode?
|
||||||
|
for node in self.tokenNodes {
|
||||||
|
if node.token.id == token.id {
|
||||||
|
currentNode = node
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tokenNode: TokenNode
|
||||||
|
var animateIn = false
|
||||||
|
if let currentNode = currentNode {
|
||||||
|
tokenNode = currentNode
|
||||||
|
} else {
|
||||||
|
tokenNode = TokenNode(context: self.context, presentationTheme: self.presentationTheme, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
|
||||||
|
self.tokenNodes.append(tokenNode)
|
||||||
|
self.scrollNode.addSubnode(tokenNode)
|
||||||
|
animateIn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenSize = tokenNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
|
||||||
|
if tokenSize.width + currentOffset.x >= width - sideInset && !currentOffset.x.isEqual(to: sideInset) {
|
||||||
|
currentOffset.x = sideInset
|
||||||
|
currentOffset.y += tokenSize.height
|
||||||
|
}
|
||||||
|
let tokenFrame = CGRect(origin: CGPoint(x: currentOffset.x, y: currentOffset.y), size: tokenSize)
|
||||||
|
currentOffset.x += ceil(tokenSize.width)
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
tokenNode.frame = tokenFrame
|
||||||
|
tokenNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
tokenNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||||
|
} else {
|
||||||
|
if case .immediate = transition {
|
||||||
|
transition.updateFrame(node: tokenNode, frame: tokenFrame)
|
||||||
|
} else {
|
||||||
|
let previousFrame = tokenNode.frame
|
||||||
|
if !previousFrame.origin.y.isEqual(to: tokenFrame.origin.y) && previousFrame.size.width.isEqual(to: tokenFrame.size.width) {
|
||||||
|
let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY)
|
||||||
|
let initialEndPosition = CGPoint(x: previousFrame.midY > tokenFrame.midY ? -previousFrame.size.width / 2.0 : width, y: previousFrame.midY)
|
||||||
|
let targetStartPosition = CGPoint(x: (previousFrame.midY > tokenFrame.midY ? (width + tokenFrame.size.width) : -tokenFrame.size.width), y: tokenFrame.midY)
|
||||||
|
let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY)
|
||||||
|
tokenNode.frame = tokenFrame
|
||||||
|
|
||||||
|
let initialAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: initialEndPosition), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.12, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
|
||||||
|
let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: targetStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
|
||||||
|
tokenNode.layer.animateGroup([initialAnimation, targetAnimation], key: "slide")
|
||||||
|
animationDelay += 0.025
|
||||||
|
} else {
|
||||||
|
if !previousFrame.size.width.isEqual(to: tokenFrame.size.width) {
|
||||||
|
tokenNode.frame = tokenFrame
|
||||||
|
} else {
|
||||||
|
let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY)
|
||||||
|
let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY)
|
||||||
|
tokenNode.frame = tokenFrame
|
||||||
|
|
||||||
|
let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
|
||||||
|
tokenNode.layer.animateGroup([targetAnimation], key: "slide")
|
||||||
|
animationDelay += 0.025
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
|
||||||
|
if width - currentOffset.x < placeholderSize.width {
|
||||||
|
currentOffset.y += 28.0
|
||||||
|
currentOffset.x = sideInset
|
||||||
|
}
|
||||||
|
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||||
|
|
||||||
|
let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0))
|
||||||
|
let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel))
|
||||||
|
if case .immediate = transition {
|
||||||
|
transition.updateFrame(node: self.textFieldScrollNode, frame: textNodeFrame)
|
||||||
|
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
|
||||||
|
transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame)
|
||||||
|
} else {
|
||||||
|
let previousFrame = self.textFieldScrollNode.frame
|
||||||
|
self.textFieldScrollNode.frame = textNodeFrame
|
||||||
|
self.textFieldScrollNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
|
||||||
|
|
||||||
|
let previousCaretFrame = self.caretIndicatorNode.frame
|
||||||
|
self.caretIndicatorNode.frame = caretNodeFrame
|
||||||
|
self.caretIndicatorNode.layer.animateFrame(from: previousCaretFrame, to: caretNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousContentHeight = self.scrollNode.view.contentSize.height
|
||||||
|
let contentHeight = currentOffset.y + 29.0 + verticalInset
|
||||||
|
let nodeHeight = min(contentHeight, 110.0)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
||||||
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
|
||||||
|
|
||||||
|
if !abs(previousContentHeight - contentHeight).isLess(than: CGFloat.ulpOfOne) {
|
||||||
|
let contentOffset = CGPoint(x: 0.0, y: max(0.0, contentHeight - nodeHeight))
|
||||||
|
if case .immediate = transition {
|
||||||
|
self.scrollNode.view.contentOffset = contentOffset
|
||||||
|
} else {
|
||||||
|
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: self.scrollNode.bounds.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
|
||||||
|
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
|
||||||
|
|
||||||
|
return nodeHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func textFieldChanged(_ textField: UITextField) {
|
||||||
|
let text = textField.text ?? ""
|
||||||
|
self.placeholderNode.isHidden = !text.isEmpty
|
||||||
|
self.updateSelectedTokenId(nil)
|
||||||
|
self.textUpdated?(text)
|
||||||
|
if !text.isEmpty {
|
||||||
|
self.scrollNode.view.scrollRectToVisible(textFieldScrollNode.frame.offsetBy(dx: 0.0, dy: 7.0), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
self.textReturned?()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
self.isFirstResponderChanged?()
|
||||||
|
/*if self.caretIndicatorNode.supernode == self {
|
||||||
|
self.caretIndicatorNode.removeFromSupernode()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
self.isFirstResponderChanged?()
|
||||||
|
/*if self.caretIndicatorNode.supernode != self.scrollNode {
|
||||||
|
self.scrollNode.addSubnode(self.caretIndicatorNode)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func setText(_ text: String) {
|
||||||
|
self.textFieldNode.textField.text = text
|
||||||
|
self.textFieldChanged(self.textFieldNode.textField)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSelectedTokenId(_ id: AnyHashable?, animated: Bool = false) {
|
||||||
|
self.selectedTokenId = id
|
||||||
|
for tokenNode in self.tokenNodes {
|
||||||
|
tokenNode.updateIsSelected(id == tokenNode.token.id, animated: animated)
|
||||||
|
}
|
||||||
|
if id != nil && !self.textFieldNode.textField.isFirstResponder {
|
||||||
|
self.textFieldNode.textField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
let point = recognizer.location(in: self.view)
|
||||||
|
for tokenNode in self.tokenNodes {
|
||||||
|
let convertedPoint = self.view.convert(point, to: tokenNode.view)
|
||||||
|
if tokenNode.bounds.contains(convertedPoint) {
|
||||||
|
if tokenNode.isSelected {
|
||||||
|
self.deleteToken?(tokenNode.token.id)
|
||||||
|
} else {
|
||||||
|
self.updateSelectedTokenId(tokenNode.token.id, animated: true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import AccountContext
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import TelegramCore
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
|
||||||
|
public final class TokenListTextField: Component {
|
||||||
|
public final class ExternalState {
|
||||||
|
public fileprivate(set) var isFocused: Bool = false
|
||||||
|
public fileprivate(set) var text: String = ""
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class Token: Equatable {
|
||||||
|
public enum Content: Equatable {
|
||||||
|
case peer(EnginePeer)
|
||||||
|
case category(UIImage?)
|
||||||
|
|
||||||
|
public static func ==(lhs: Content, rhs: Content) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .peer(peer):
|
||||||
|
if case .peer(peer) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .category(lhsImage):
|
||||||
|
if case let .category(rhsImage) = rhs, lhsImage === rhsImage {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public let id: AnyHashable
|
||||||
|
public let title: String
|
||||||
|
public let fixedPosition: Int?
|
||||||
|
public let content: Content
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: AnyHashable,
|
||||||
|
title: String,
|
||||||
|
fixedPosition: Int?,
|
||||||
|
content: Content
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.title = title
|
||||||
|
self.fixedPosition = fixedPosition
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: Token, rhs: Token) -> Bool {
|
||||||
|
if lhs.id != rhs.id {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.fixedPosition != rhs.fixedPosition {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.content != rhs.content {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public let externalState: ExternalState
|
||||||
|
public let context: AccountContext
|
||||||
|
public let theme: PresentationTheme
|
||||||
|
public let placeholder: String
|
||||||
|
public let tokens: [Token]
|
||||||
|
public let sideInset: CGFloat
|
||||||
|
public let deleteToken: (AnyHashable) -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
externalState: ExternalState,
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
placeholder: String,
|
||||||
|
tokens: [Token],
|
||||||
|
sideInset: CGFloat,
|
||||||
|
deleteToken: @escaping (AnyHashable) -> Void
|
||||||
|
) {
|
||||||
|
self.externalState = externalState
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self.tokens = tokens
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.deleteToken = deleteToken
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: TokenListTextField, rhs: TokenListTextField) -> Bool {
|
||||||
|
if lhs.externalState !== rhs.externalState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.placeholder != rhs.placeholder {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.tokens != rhs.tokens {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class View: UIView {
|
||||||
|
private var tokenListNode: EditableTokenListNode?
|
||||||
|
|
||||||
|
private var tokenListText: String = ""
|
||||||
|
|
||||||
|
private var component: TokenListTextField?
|
||||||
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let result = super.hitTest(point, with: event)
|
||||||
|
if result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearText() {
|
||||||
|
if let tokenListNode = self.tokenListNode {
|
||||||
|
tokenListNode.setText("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: TokenListTextField, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
self.componentState = state
|
||||||
|
|
||||||
|
let tokenListNode: EditableTokenListNode
|
||||||
|
if let current = self.tokenListNode {
|
||||||
|
tokenListNode = current
|
||||||
|
} else {
|
||||||
|
tokenListNode = EditableTokenListNode(
|
||||||
|
context: component.context,
|
||||||
|
presentationTheme: component.theme,
|
||||||
|
theme: EditableTokenListNodeTheme(
|
||||||
|
backgroundColor: .clear,
|
||||||
|
separatorColor: component.theme.rootController.navigationBar.separatorColor,
|
||||||
|
placeholderTextColor: component.theme.list.itemPlaceholderTextColor,
|
||||||
|
primaryTextColor: component.theme.list.itemPrimaryTextColor,
|
||||||
|
tokenBackgroundColor: component.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25),
|
||||||
|
selectedTextColor: component.theme.list.itemCheckColors.foregroundColor,
|
||||||
|
selectedBackgroundColor: component.theme.list.itemCheckColors.fillColor,
|
||||||
|
accentColor: component.theme.list.itemAccentColor,
|
||||||
|
keyboardColor: component.theme.rootController.keyboardColor
|
||||||
|
),
|
||||||
|
placeholder: component.placeholder
|
||||||
|
)
|
||||||
|
self.tokenListNode = tokenListNode
|
||||||
|
self.addSubnode(tokenListNode)
|
||||||
|
|
||||||
|
tokenListNode.isFirstResponderChanged = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.componentState?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenListNode.textUpdated = { [weak self] text in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.tokenListText = text
|
||||||
|
self.componentState?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenListNode.textReturned = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.tokenListNode?.view.endEditing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenListNode.deleteToken = { [weak self] id in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.deleteToken(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedTokens = component.tokens.map { token -> EditableTokenListToken in
|
||||||
|
let mappedSubject: EditableTokenListToken.Subject
|
||||||
|
switch token.content {
|
||||||
|
case let .peer(peer):
|
||||||
|
mappedSubject = .peer(peer)
|
||||||
|
case let .category(image):
|
||||||
|
mappedSubject = .category(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
return EditableTokenListToken(
|
||||||
|
id: token.id,
|
||||||
|
title: token.title,
|
||||||
|
fixedPosition: token.fixedPosition,
|
||||||
|
subject: mappedSubject
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = tokenListNode.updateLayout(
|
||||||
|
tokens: mappedTokens,
|
||||||
|
width: availableSize.width,
|
||||||
|
leftInset: component.sideInset,
|
||||||
|
rightInset: component.sideInset,
|
||||||
|
transition: transition.containedViewLayoutTransition
|
||||||
|
)
|
||||||
|
let size = CGSize(width: availableSize.width, height: height)
|
||||||
|
transition.containedViewLayoutTransition.updateFrame(node: tokenListNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
component.externalState.isFocused = tokenListNode.isFocused
|
||||||
|
component.externalState.text = self.tokenListText
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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)
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ import LegacyMediaPickerUI
|
|||||||
import LegacyCamera
|
import LegacyCamera
|
||||||
import AvatarNode
|
import AvatarNode
|
||||||
import LocalMediaResources
|
import LocalMediaResources
|
||||||
|
import ShareWithPeersScreen
|
||||||
|
|
||||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
@ -349,22 +350,21 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}, completion: { mediaResult, commit in
|
}, completion: { mediaResult, commit in
|
||||||
let privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
let stateContext = ShareWithPeersScreen.StateContext(context: self.context)
|
||||||
// if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) {
|
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||||
// privacy.base = .everyone
|
guard let self else {
|
||||||
// } else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) {
|
return
|
||||||
// privacy.base = .contacts
|
}
|
||||||
// } else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) {
|
guard let controller = self.viewControllers.last as? ViewController else {
|
||||||
// privacy.base = .closeFriends
|
return
|
||||||
// }
|
}
|
||||||
// privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in
|
|
||||||
// switch id {
|
controller.push(ShareWithPeersScreen(context: self.context, stateContext: stateContext, completion: { [weak self] privacy in
|
||||||
// case let .peer(peerId):
|
guard let self else {
|
||||||
// return peerId
|
dismissCameraImpl?()
|
||||||
// default:
|
commit()
|
||||||
// return nil
|
return
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
|
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
|
||||||
switch mediaResult {
|
switch mediaResult {
|
||||||
@ -398,8 +398,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissCameraImpl?()
|
dismissCameraImpl?()
|
||||||
commit()
|
commit()
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
controller.sourceHint = .camera
|
controller.sourceHint = .camera
|
||||||
controller.cancelled = {
|
controller.cancelled = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user