Skip to content

Commit b308f42

Browse files
committed
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
2 parents 50fe471 + 765aa49 commit b308f42

File tree

5 files changed

+378
-16
lines changed

5 files changed

+378
-16
lines changed

submodules/TelegramUI/Components/ListActionItemComponent/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ swift_library(
1616
"//submodules/TelegramUI/Components/ListSectionComponent",
1717
"//submodules/SwitchNode",
1818
"//submodules/CheckNode",
19+
"//submodules/Components/MultilineTextComponent",
1920
],
2021
visibility = [
2122
"//visibility:public",
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import Foundation
2+
import UIKit
3+
import Display
4+
import ComponentFlow
5+
import MultilineTextComponent
6+
7+
private final class ContextOptionComponent: Component {
8+
let title: String
9+
let color: UIColor
10+
let isLast: Bool
11+
let action: () -> Void
12+
13+
init(
14+
title: String,
15+
color: UIColor,
16+
isLast: Bool,
17+
action: @escaping () -> Void
18+
) {
19+
self.title = title
20+
self.color = color
21+
self.isLast = isLast
22+
self.action = action
23+
}
24+
25+
static func ==(lhs: ContextOptionComponent, rhs: ContextOptionComponent) -> Bool {
26+
if lhs.title != rhs.title {
27+
return false
28+
}
29+
if lhs.color != rhs.color {
30+
return false
31+
}
32+
if lhs.isLast != rhs.isLast {
33+
return false
34+
}
35+
return true
36+
}
37+
38+
final class View: UIView {
39+
let backgroundView: UIView
40+
let title = ComponentView<Empty>()
41+
42+
var component: ContextOptionComponent?
43+
44+
override init(frame: CGRect) {
45+
self.backgroundView = UIView()
46+
47+
super.init(frame: frame)
48+
49+
self.addSubview(self.backgroundView)
50+
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
51+
}
52+
53+
required init?(coder: NSCoder) {
54+
fatalError("init(coder:) has not been implemented")
55+
}
56+
57+
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
58+
if case .ended = recognizer.state {
59+
self.component?.action()
60+
}
61+
}
62+
63+
func update(component: ContextOptionComponent, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
64+
self.component = component
65+
66+
let sideInset: CGFloat = 8.0
67+
68+
let titleSize = self.title.update(
69+
transition: .immediate,
70+
component: AnyComponent(MultilineTextComponent(
71+
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: .white))
72+
)),
73+
environment: {},
74+
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
75+
)
76+
let size = CGSize(width: sideInset * 2.0 + titleSize.width, height: availableSize.height)
77+
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((size.height - titleSize.height) * 0.5)), size: titleSize)
78+
if let titleView = self.title.view {
79+
if titleView.superview == nil {
80+
self.addSubview(titleView)
81+
}
82+
transition.setPosition(view: titleView, position: titleFrame.center)
83+
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
84+
}
85+
86+
self.backgroundView.backgroundColor = component.color
87+
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width + (component.isLast ? 1000.0 : 0.0), height: size.height)))
88+
89+
return size
90+
}
91+
}
92+
93+
func makeView() -> View {
94+
return View(frame: CGRect())
95+
}
96+
97+
func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
98+
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
99+
}
100+
}
101+
102+
final class ContentContainer: UIScrollView, UIScrollViewDelegate {
103+
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
104+
105+
private var ignoreScrollingEvents: Bool = false
106+
private var draggingBeganInClosedState: Bool = false
107+
108+
private var contextOptions: [ListActionItemComponent.ContextOption] = []
109+
private var optionsWidth: CGFloat = 0.0
110+
111+
private var revealedStateTapRecognizer: UITapGestureRecognizer?
112+
113+
override init(frame: CGRect) {
114+
super.init(frame: frame)
115+
116+
let revealedStateTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))
117+
self.revealedStateTapRecognizer = revealedStateTapRecognizer
118+
revealedStateTapRecognizer.isEnabled = false
119+
self.addGestureRecognizer(revealedStateTapRecognizer)
120+
121+
self.delaysContentTouches = false
122+
self.canCancelContentTouches = true
123+
self.clipsToBounds = false
124+
self.contentInsetAdjustmentBehavior = .never
125+
self.automaticallyAdjustsScrollIndicatorInsets = false
126+
self.showsVerticalScrollIndicator = false
127+
self.showsHorizontalScrollIndicator = false
128+
self.alwaysBounceHorizontal = false
129+
self.alwaysBounceVertical = false
130+
self.scrollsToTop = false
131+
self.delegate = self
132+
133+
self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
134+
guard let self else {
135+
return false
136+
}
137+
138+
if self.contentOffset.x != 0.0 {
139+
return true
140+
}
141+
142+
return false
143+
}
144+
}
145+
146+
required init?(coder: NSCoder) {
147+
fatalError("init(coder:) has not been implemented")
148+
}
149+
150+
@objc func onTapGesture(_ recognizer: UITapGestureRecognizer) {
151+
if case .ended = recognizer.state {
152+
self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
153+
}
154+
}
155+
156+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
157+
self.revealedStateTapRecognizer?.isEnabled = self.contentOffset.x > 0.0
158+
}
159+
160+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
161+
self.draggingBeganInClosedState = self.contentOffset.x == 0.0
162+
}
163+
164+
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
165+
targetContentOffset.pointee.x = self.contentOffset.x
166+
167+
if self.contentOffset.x >= self.optionsWidth + 30.0 {
168+
self.contextOptions.last?.action()
169+
} else {
170+
DispatchQueue.main.async { [weak self] in
171+
guard let self else {
172+
return
173+
}
174+
if self.draggingBeganInClosedState {
175+
if self.contentOffset.x > 20.0 {
176+
self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true)
177+
} else {
178+
self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
179+
}
180+
} else {
181+
if self.contentOffset.x < self.optionsWidth - 20.0 {
182+
self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
183+
} else {
184+
self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true)
185+
}
186+
}
187+
}
188+
}
189+
}
190+
191+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
192+
if let revealedStateTapRecognizer = self.revealedStateTapRecognizer, revealedStateTapRecognizer.isEnabled {
193+
if self.bounds.contains(point), point.x < self.bounds.width {
194+
return self
195+
}
196+
}
197+
guard let result = super.hitTest(point, with: event) else {
198+
return nil
199+
}
200+
return result
201+
}
202+
203+
func update(size: CGSize, contextOptions: [ListActionItemComponent.ContextOption], transition: ComponentTransition) {
204+
self.contextOptions = contextOptions
205+
206+
var validIds: [AnyHashable] = []
207+
var optionsWidth: CGFloat = 0.0
208+
for i in 0 ..< contextOptions.count {
209+
let option = contextOptions[i]
210+
validIds.append(option.id)
211+
212+
let itemView: ComponentView<Empty>
213+
var itemTransition = transition
214+
if let current = self.itemViews[option.id] {
215+
itemView = current
216+
} else {
217+
itemTransition = itemTransition.withAnimation(.none)
218+
itemView = ComponentView()
219+
self.itemViews[option.id] = itemView
220+
}
221+
222+
let itemSize = itemView.update(
223+
transition: itemTransition,
224+
component: AnyComponent(ContextOptionComponent(
225+
title: option.title,
226+
color: option.color,
227+
isLast: i == contextOptions.count - 1,
228+
action: option.action
229+
)),
230+
environment: {},
231+
containerSize: CGSize(width: 10000.0, height: size.height)
232+
)
233+
let itemFrame = CGRect(origin: CGPoint(x: size.width + optionsWidth, y: 0.0), size: itemSize)
234+
optionsWidth += itemSize.width
235+
if let itemComponentView = itemView.view {
236+
self.addSubview(itemComponentView)
237+
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
238+
}
239+
}
240+
var removedIds: [AnyHashable] = []
241+
for (id, itemView) in self.itemViews {
242+
if !validIds.contains(id) {
243+
removedIds.append(id)
244+
if let itemComponentView = itemView.view {
245+
itemComponentView.removeFromSuperview()
246+
}
247+
}
248+
}
249+
for id in removedIds {
250+
self.itemViews.removeValue(forKey: id)
251+
}
252+
self.optionsWidth = optionsWidth
253+
254+
let contentSize = CGSize(width: size.width + optionsWidth, height: size.height)
255+
if self.contentSize != contentSize {
256+
self.contentSize = contentSize
257+
}
258+
self.isScrollEnabled = optionsWidth != 0.0
259+
}
260+
}

0 commit comments

Comments
 (0)