mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories
This commit is contained in:
parent
0d39380cdf
commit
069f1aa475
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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 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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user