[WIP] Stories

This commit is contained in:
Ali 2023-05-16 18:15:31 +04:00
parent 0d39380cdf
commit 069f1aa475
11 changed files with 2185 additions and 1313 deletions

View File

@ -368,6 +368,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/FullScreenEffectView",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -28,8 +28,10 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
"//submodules/TelegramUI/Components/TokenListTextField",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/PeerPresenceStatusManager",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -11,6 +11,7 @@ import AvatarNode
import TelegramPresentationData
import CheckNode
import TelegramStringFormatting
import PeerPresenceStatusManager
private let avatarFont = avatarPlaceholderFont(size: 15.0)
@ -27,6 +28,7 @@ final class PeerListItemComponent: Component {
let title: String
let peer: EnginePeer?
let subtitle: String?
let presence: EnginePeer.Presence?
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
@ -39,6 +41,7 @@ final class PeerListItemComponent: Component {
title: String,
peer: EnginePeer?,
subtitle: String?,
presence: EnginePeer.Presence?,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void
@ -50,6 +53,7 @@ final class PeerListItemComponent: Component {
self.title = title
self.peer = peer
self.subtitle = subtitle
self.presence = presence
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
@ -77,6 +81,9 @@ final class PeerListItemComponent: Component {
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.presence != rhs.presence {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
@ -99,6 +106,8 @@ final class PeerListItemComponent: Component {
private var component: PeerListItemComponent?
private weak var state: EmptyComponentState?
private var presenceManager: PeerPresenceStatusManager?
override init(frame: CGRect) {
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 {
let animationHint = transition.userData(ShareWithPeersScreenComponent.AnimationHint.self)
var synchronousLoad = false
if let animationHint, animationHint.contentReloaded {
synchronousLoad = true
}
let themeUpdated = self.component?.theme !== component.theme
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.state = state
@ -210,15 +242,15 @@ final class PeerListItemComponent: Component {
} else {
clipStyle = .round
}
if peer.id == component.context.account.peerId {
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))
}
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
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)
} else if case .legacyGroup = component.peer {
labelData = (component.strings.Group_Status, false)

View File

@ -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)
}
}

View 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",
],
)

View File

@ -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
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -23,6 +23,7 @@ import LegacyMediaPickerUI
import LegacyCamera
import AvatarNode
import LocalMediaResources
import ShareWithPeersScreen
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData
@ -257,6 +258,22 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
controller.view.endEditing(true)
if !"".isEmpty {
let stateContext = ShareWithPeersScreen.StateContext(context: self.context)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
guard let controller = self.viewControllers.last as? ViewController else {
return
}
controller.push(ShareWithPeersScreen(context: self.context, stateContext: stateContext, completion: { _ in
}))
})
return
}
var presentImpl: ((ViewController) -> Void)?
var dismissCameraImpl: (() -> Void)?
let cameraController = CameraScreen(context: self.context, mode: .story, completion: { [weak self] result in
@ -272,13 +289,68 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
let context = self.context
legacyStoryMediaEditor(context: context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in
dismissCameraImpl?()
guard let self else {
return
}
enum AdditionalCategoryId: Int {
let stateContext = ShareWithPeersScreen.StateContext(context: self.context)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
guard let controller = self.viewControllers.last as? ViewController else {
return
}
controller.push(ShareWithPeersScreen(context: self.context, stateContext: stateContext, completion: { privacy in
switch mediaResult {
case let .image(image):
_ = image
break
case let .video(path):
_ = path
break
case let .asset(asset):
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
switch asset.mediaType {
case .image:
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
if let data, let image = UIImage(data: data) {
Queue.mainQueue().async {
guard let self else {
return
}
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
storyListContext.upload(media: .image(dimensions: PixelDimensions(image.size), data: data), text: "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
}
}
})
case .video:
let resource = VideoLibraryMediaResource(localIdentifier: asset.localIdentifier, conversion: VideoLibraryMediaResourceConversion.passthrough)
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
storyListContext.upload(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), text: "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
default:
break
}
}
dismissCameraImpl?()
}))
})
/*enum AdditionalCategoryId: Int {
case everyone
case contacts
case closeFriends
@ -371,6 +443,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
guard let self else {
return
}
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
storyListContext.upload(media: .image(dimensions: PixelDimensions(image.size), data: data), text: "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
@ -395,7 +468,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
selectionController?.dismiss()
}
}
})
})*/
}, present: { c, a in
presentImpl?(c)
})