mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Business fixes
This commit is contained in:
parent
addc4dc436
commit
9ce2c2dc9e
@ -348,6 +348,9 @@ public final class AccountViewTracker {
|
||||
|
||||
var resetPeerHoleManagement: ((PeerId) -> Void)?
|
||||
|
||||
private var quickRepliesUpdateDisposable: Disposable?
|
||||
private var quickRepliesUpdateTimestamp: Double = 0.0
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
self.accountPeerId = account.peerId
|
||||
@ -373,6 +376,7 @@ public final class AccountViewTracker {
|
||||
self.updatedViewCountDisposables.dispose()
|
||||
self.updatedReactionsDisposables.dispose()
|
||||
self.externallyUpdatedPeerIdDisposable.dispose()
|
||||
self.quickRepliesUpdateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
@ -2548,6 +2552,20 @@ public final class AccountViewTracker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func keepQuickRepliesApproximatelyUpdated() {
|
||||
self.queue.async {
|
||||
guard let account = self.account else {
|
||||
return
|
||||
}
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
if self.quickRepliesUpdateTimestamp + 16 * 60 * 60 < timestamp {
|
||||
self.quickRepliesUpdateTimestamp = timestamp
|
||||
self.quickRepliesUpdateDisposable?.dispose()
|
||||
self.quickRepliesUpdateDisposable = _internal_keepShortcutMessagesUpdated(account: account).startStrict()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExtractedChatListItemCachedData: Hashable {
|
||||
|
@ -1414,6 +1414,7 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId:
|
||||
state.filters = remoteFilters
|
||||
state.remoteFilters = state.filters
|
||||
state.displayTags = remoteTagsEnabled
|
||||
state.remoteDisplayTags = state.displayTags
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ public final class ListActionItemComponent: Component {
|
||||
contentHeight += component.contentInsets.top
|
||||
|
||||
if component.leftIcon != nil {
|
||||
contentLeftInset += 52.0
|
||||
contentLeftInset += 46.0
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
|
@ -0,0 +1,21 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ListItemSwipeOptionContainer",
|
||||
module_name = "ListItemSwipeOptionContainer",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,905 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import AppBundle
|
||||
import MultilineTextComponent
|
||||
|
||||
private let titleFontWithIcon = Font.medium(13.0)
|
||||
private let titleFontWithoutIcon = Font.regular(17.0)
|
||||
|
||||
private final class SwipeOptionsGestureRecognizer: UIPanGestureRecognizer {
|
||||
public var validatedGesture = false
|
||||
public var firstLocation: CGPoint = CGPoint()
|
||||
|
||||
public var allowAnyDirection = false
|
||||
public var lastVelocity: CGPoint = CGPoint()
|
||||
|
||||
override public init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
if #available(iOS 13.4, *) {
|
||||
self.allowedScrollTypesMask = .continuous
|
||||
}
|
||||
|
||||
self.maximumNumberOfTouches = 1
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.validatedGesture = false
|
||||
}
|
||||
|
||||
public func becomeCancelled() {
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let touch = touches.first!
|
||||
self.firstLocation = touch.location(in: self.view)
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
let location = touches.first!.location(in: self.view)
|
||||
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||
|
||||
if !self.validatedGesture {
|
||||
if !self.allowAnyDirection && translation.x > 0.0 {
|
||||
self.state = .failed
|
||||
} else if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||
self.state = .failed
|
||||
} else if abs(translation.x) > 4.0 && abs(translation.y) * 2.5 < abs(translation.x) {
|
||||
self.validatedGesture = true
|
||||
}
|
||||
}
|
||||
|
||||
if self.validatedGesture {
|
||||
self.lastVelocity = self.velocity(in: self.view)
|
||||
super.touchesMoved(touches, with: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate {
|
||||
public struct Option: Equatable {
|
||||
public enum Icon: Equatable {
|
||||
case none
|
||||
case image(image: UIImage)
|
||||
|
||||
public static func ==(lhs: Icon, rhs: Icon) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .image(lhsImage):
|
||||
if case let .image(rhsImage) = rhs, lhsImage == rhsImage {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let key: AnyHashable
|
||||
public let title: String
|
||||
public let icon: Icon
|
||||
public let color: UIColor
|
||||
public let textColor: UIColor
|
||||
|
||||
public init(key: AnyHashable, title: String, icon: Icon, color: UIColor, textColor: UIColor) {
|
||||
self.key = key
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.color = color
|
||||
self.textColor = textColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: Option, rhs: Option) -> Bool {
|
||||
if lhs.key != rhs.key {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if !lhs.textColor.isEqual(rhs.textColor) {
|
||||
return false
|
||||
}
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private enum OptionAlignment {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
private final class OptionView: UIView {
|
||||
private let backgroundView: UIView
|
||||
private let title = ComponentView<Empty>()
|
||||
private var iconView: UIImageView?
|
||||
|
||||
private let titleString: String
|
||||
private let textColor: UIColor
|
||||
|
||||
private var titleSize: CGSize?
|
||||
|
||||
var alignment: OptionAlignment?
|
||||
var isExpanded: Bool = false
|
||||
|
||||
init(title: String, icon: Option.Icon, color: UIColor, textColor: UIColor) {
|
||||
self.titleString = title
|
||||
self.textColor = textColor
|
||||
|
||||
self.backgroundView = UIView()
|
||||
|
||||
switch icon {
|
||||
case let .image(image):
|
||||
let iconView = UIImageView()
|
||||
iconView.image = image.withRenderingMode(.alwaysTemplate)
|
||||
iconView.tintColor = textColor
|
||||
self.iconView = iconView
|
||||
case .none:
|
||||
self.iconView = nil
|
||||
}
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
if let iconView = self.iconView {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
self.backgroundView.backgroundColor = color
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateLayout(
|
||||
isFirst: Bool,
|
||||
isLeft: Bool,
|
||||
baseSize: CGSize,
|
||||
alignment: OptionAlignment,
|
||||
isExpanded: Bool,
|
||||
extendedWidth: CGFloat,
|
||||
sideInset: CGFloat,
|
||||
transition: Transition,
|
||||
additive: Bool,
|
||||
revealFactor: CGFloat,
|
||||
animateIconMovement: Bool
|
||||
) {
|
||||
var animateAdditive = false
|
||||
if additive && !transition.animation.isImmediate && self.isExpanded != isExpanded {
|
||||
animateAdditive = true
|
||||
}
|
||||
|
||||
let backgroundFrame: CGRect
|
||||
if isFirst {
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: isLeft ? -400.0 : 0.0, y: 0.0), size: CGSize(width: extendedWidth + 400.0, height: baseSize.height))
|
||||
} else {
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: extendedWidth, height: baseSize.height))
|
||||
}
|
||||
let deltaX: CGFloat
|
||||
if animateAdditive {
|
||||
let previousFrame = self.backgroundView.frame
|
||||
self.backgroundView.frame = backgroundFrame
|
||||
if isLeft {
|
||||
deltaX = previousFrame.width - backgroundFrame.width
|
||||
} else {
|
||||
deltaX = -(previousFrame.width - backgroundFrame.width)
|
||||
}
|
||||
if !animateIconMovement {
|
||||
transition.animatePosition(view: self.backgroundView, from: CGPoint(x: deltaX, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
} else {
|
||||
deltaX = 0.0
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
self.alignment = alignment
|
||||
self.isExpanded = isExpanded
|
||||
let titleSize = self.titleSize ?? CGSize(width: 32.0, height: 10.0)
|
||||
var contentRect = CGRect(origin: CGPoint(), size: baseSize)
|
||||
switch alignment {
|
||||
case .left:
|
||||
contentRect.origin.x = 0.0
|
||||
case .right:
|
||||
contentRect.origin.x = extendedWidth - contentRect.width
|
||||
}
|
||||
|
||||
if let iconView = self.iconView, let imageSize = iconView.image?.size {
|
||||
let iconOffset: CGFloat = -9.0
|
||||
let titleIconSpacing: CGFloat = 11.0
|
||||
let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize)
|
||||
if animateAdditive {
|
||||
let iconOffsetX = animateIconMovement ? iconView.frame.minX - iconFrame.minX : deltaX
|
||||
iconView.frame = iconFrame
|
||||
transition.animatePosition(view: iconView, from: CGPoint(x: iconOffsetX, y: 0.0), to: CGPoint(), additive: true)
|
||||
} else {
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
if animateAdditive {
|
||||
let titleOffsetX = animateIconMovement ? titleView.frame.minX - titleFrame.minX : deltaX
|
||||
titleView.frame = titleFrame
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: titleOffsetX, y: 0.0), to: CGPoint(), additive: true)
|
||||
} else {
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.minY + floor((baseSize.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
if animateAdditive {
|
||||
let titleOffsetX = animateIconMovement ? titleView.frame.minX - titleFrame.minX : deltaX
|
||||
titleView.frame = titleFrame
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: titleOffsetX, y: 0.0), to: CGPoint(), additive: true)
|
||||
} else {
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateSize(_ constrainedSize: CGSize) -> CGSize {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: self.titleString, font: self.iconView == nil ? titleFontWithoutIcon : titleFontWithIcon, textColor: self.textColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||
)
|
||||
self.titleSize = titleSize
|
||||
|
||||
var maxWidth = titleSize.width
|
||||
if let iconView = self.iconView, let image = iconView.image {
|
||||
maxWidth = max(image.size.width, maxWidth)
|
||||
}
|
||||
return CGSize(width: max(74.0, maxWidth + 20.0), height: constrainedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
public final class OptionsView: UIView {
|
||||
private let optionSelected: (Option) -> Void
|
||||
private let tapticAction: () -> Void
|
||||
|
||||
private var options: [Option] = []
|
||||
private var isLeft: Bool = false
|
||||
|
||||
private var optionViews: [OptionView] = []
|
||||
private var revealOffset: CGFloat = 0.0
|
||||
private var sideInset: CGFloat = 0.0
|
||||
|
||||
public init(optionSelected: @escaping (Option) -> Void, tapticAction: @escaping () -> Void) {
|
||||
self.optionSelected = optionSelected
|
||||
self.tapticAction = tapticAction
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
let gestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
gestureRecognizer.tapActionAtPoint = { _ in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
self.addGestureRecognizer(gestureRecognizer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func setOptions(_ options: [Option], isLeft: Bool) {
|
||||
if self.options != options || self.isLeft != isLeft {
|
||||
self.options = options
|
||||
self.isLeft = isLeft
|
||||
for optionView in self.optionViews {
|
||||
optionView.removeFromSuperview()
|
||||
}
|
||||
self.optionViews = options.map { option in
|
||||
return OptionView(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor)
|
||||
}
|
||||
if isLeft {
|
||||
for optionView in self.optionViews.reversed() {
|
||||
self.addSubview(optionView)
|
||||
}
|
||||
} else {
|
||||
for optionView in self.optionViews {
|
||||
self.addSubview(optionView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateSize(_ constrainedSize: CGSize) -> CGSize {
|
||||
var maxWidth: CGFloat = 0.0
|
||||
for optionView in self.optionViews {
|
||||
let nodeSize = optionView.calculateSize(constrainedSize)
|
||||
maxWidth = max(nodeSize.width, maxWidth)
|
||||
}
|
||||
return CGSize(width: maxWidth * CGFloat(self.optionViews.count), height: constrainedSize.height)
|
||||
}
|
||||
|
||||
public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: Transition) {
|
||||
self.revealOffset = offset
|
||||
self.sideInset = sideInset
|
||||
self.updateNodesLayout(transition: transition)
|
||||
}
|
||||
|
||||
private func updateNodesLayout(transition: Transition) {
|
||||
let size = self.bounds.size
|
||||
if size.width.isLessThanOrEqualTo(0.0) || self.optionViews.isEmpty {
|
||||
return
|
||||
}
|
||||
let basicNodeWidth = floor((size.width - abs(self.sideInset)) / CGFloat(self.optionViews.count))
|
||||
let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionViews.count - 1)
|
||||
let revealFactor = self.revealOffset / size.width
|
||||
let boundaryRevealFactor: CGFloat
|
||||
if self.optionViews.count > 2 {
|
||||
boundaryRevealFactor = 1.0 + 16.0 / size.width
|
||||
} else {
|
||||
boundaryRevealFactor = 1.0 + basicNodeWidth / size.width
|
||||
}
|
||||
let startingOffset: CGFloat
|
||||
if self.isLeft {
|
||||
startingOffset = size.width + max(0.0, abs(revealFactor) - 1.0) * size.width
|
||||
} else {
|
||||
startingOffset = 0.0
|
||||
}
|
||||
|
||||
var completionCount = self.optionViews.count
|
||||
let intermediateCompletion = {
|
||||
}
|
||||
|
||||
var i = self.isLeft ? (self.optionViews.count - 1) : 0
|
||||
while i >= 0 && i < self.optionViews.count {
|
||||
let optionView = self.optionViews[i]
|
||||
let nodeWidth = i == (self.optionViews.count - 1) ? lastNodeWidth : basicNodeWidth
|
||||
var nodeTransition = transition
|
||||
var isExpanded = false
|
||||
if (self.isLeft && i == 0) || (!self.isLeft && i == self.optionViews.count - 1) {
|
||||
if abs(revealFactor) > boundaryRevealFactor {
|
||||
isExpanded = true
|
||||
}
|
||||
}
|
||||
if let _ = optionView.alignment, optionView.isExpanded != isExpanded {
|
||||
nodeTransition = !transition.animation.isImmediate ? transition : .easeInOut(duration: 0.2)
|
||||
if transition.animation.isImmediate {
|
||||
self.tapticAction()
|
||||
}
|
||||
}
|
||||
|
||||
var sideInset: CGFloat = 0.0
|
||||
if i == self.optionViews.count - 1 {
|
||||
sideInset = self.sideInset
|
||||
}
|
||||
|
||||
let extendedWidth: CGFloat
|
||||
let nodeLeftOffset: CGFloat
|
||||
if isExpanded {
|
||||
nodeLeftOffset = 0.0
|
||||
extendedWidth = size.width * max(1.0, abs(revealFactor))
|
||||
} else if self.isLeft {
|
||||
let offset = basicNodeWidth * CGFloat(self.optionViews.count - 1 - i)
|
||||
extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor))
|
||||
nodeLeftOffset = startingOffset - extendedWidth - floorToScreenPixels(offset * abs(revealFactor))
|
||||
} else {
|
||||
let offset = basicNodeWidth * CGFloat(i)
|
||||
extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor))
|
||||
nodeLeftOffset = startingOffset + floorToScreenPixels(offset * abs(revealFactor))
|
||||
}
|
||||
|
||||
transition.setFrame(view: optionView, frame: CGRect(origin: CGPoint(x: nodeLeftOffset, y: 0.0), size: CGSize(width: extendedWidth, height: size.height)), completion: { _ in
|
||||
completionCount -= 1
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
var nodeAlignment: OptionAlignment
|
||||
if (self.optionViews.count > 1) {
|
||||
nodeAlignment = self.isLeft ? .right : .left
|
||||
} else {
|
||||
if self.isLeft {
|
||||
nodeAlignment = isExpanded ? .right : .left
|
||||
} else {
|
||||
nodeAlignment = isExpanded ? .left : .right
|
||||
}
|
||||
}
|
||||
let animateIconMovement = self.optionViews.count == 1
|
||||
optionView.updateLayout(isFirst: (self.isLeft && i == 0) || (!self.isLeft && i == self.optionViews.count - 1), isLeft: self.isLeft, baseSize: CGSize(width: nodeWidth, height: size.height), alignment: nodeAlignment, isExpanded: isExpanded, extendedWidth: extendedWidth, sideInset: sideInset, transition: nodeTransition, additive: transition.animation.isImmediate, revealFactor: revealFactor, animateIconMovement: animateIconMovement)
|
||||
|
||||
if self.isLeft {
|
||||
i -= 1
|
||||
} else {
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0, case .tap = gesture {
|
||||
let location = recognizer.location(in: self)
|
||||
var selectedOption: Int?
|
||||
|
||||
var i = self.isLeft ? 0 : (self.optionViews.count - 1)
|
||||
while i >= 0 && i < self.optionViews.count {
|
||||
if self.optionViews[i].frame.contains(location) {
|
||||
selectedOption = i
|
||||
break
|
||||
}
|
||||
if self.isLeft {
|
||||
i += 1
|
||||
} else {
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
if let selectedOption {
|
||||
self.optionSelected(self.options[selectedOption])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func isDisplayingExtendedAction() -> Bool {
|
||||
return self.optionViews.contains(where: { $0.isExpanded })
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: (size: CGSize, leftInset: CGFloat, reftInset: CGFloat)?
|
||||
|
||||
private var leftRevealView: OptionsView?
|
||||
private var rightRevealView: OptionsView?
|
||||
private var revealOptions: (left: [Option], right: [Option]) = ([], [])
|
||||
|
||||
private var initialRevealOffset: CGFloat = 0.0
|
||||
public private(set) var revealOffset: CGFloat = 0.0
|
||||
|
||||
private var recognizer: SwipeOptionsGestureRecognizer?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private var allowAnyDirection: Bool = false
|
||||
|
||||
public var updateRevealOffset: ((CGFloat, Transition) -> Void)?
|
||||
public var revealOptionsInteractivelyOpened: (() -> Void)?
|
||||
public var revealOptionsInteractivelyClosed: (() -> Void)?
|
||||
public var revealOptionSelected: ((Option, Bool) -> Void)?
|
||||
|
||||
open var controlsContainer: UIView {
|
||||
return self
|
||||
}
|
||||
|
||||
public var isDisplayingRevealedOptions: Bool {
|
||||
return !self.revealOffset.isZero
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let recognizer = SwipeOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
|
||||
self.recognizer = recognizer
|
||||
recognizer.delegate = self
|
||||
recognizer.allowAnyDirection = self.allowAnyDirection
|
||||
self.addGestureRecognizer(recognizer)
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.revealTapGesture(_:)))
|
||||
self.tapRecognizer = tapRecognizer
|
||||
tapRecognizer.delegate = self
|
||||
self.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizer = self.allowAnyDirection
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
if !self.revealOffset.isZero {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
open func setRevealOptions(_ options: (left: [Option], right: [Option])) {
|
||||
if self.revealOptions == options {
|
||||
return
|
||||
}
|
||||
let previousOptions = self.revealOptions
|
||||
let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty
|
||||
self.revealOptions = options
|
||||
let isEmpty = options.left.isEmpty && options.right.isEmpty
|
||||
if options.left.isEmpty {
|
||||
if let _ = self.leftRevealView {
|
||||
self.recognizer?.becomeCancelled()
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3))
|
||||
}
|
||||
} else if previousOptions.left != options.left {
|
||||
}
|
||||
if options.right.isEmpty {
|
||||
if let _ = self.rightRevealView {
|
||||
self.recognizer?.becomeCancelled()
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3))
|
||||
}
|
||||
} else if previousOptions.right != options.right {
|
||||
if let _ = self.rightRevealView {
|
||||
}
|
||||
}
|
||||
if wasEmpty != isEmpty {
|
||||
self.recognizer?.isEnabled = !isEmpty
|
||||
}
|
||||
let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero
|
||||
if allowAnyDirection != self.allowAnyDirection {
|
||||
self.allowAnyDirection = allowAnyDirection
|
||||
self.recognizer?.allowAnyDirection = allowAnyDirection
|
||||
self.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
|
||||
}
|
||||
}
|
||||
|
||||
override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let recognizer = self.recognizer, gestureRecognizer == self.tapRecognizer {
|
||||
return abs(self.revealOffset) > 0.0 && !recognizer.validatedGesture
|
||||
} else if let recognizer = self.recognizer, gestureRecognizer == self.recognizer, recognizer.numberOfTouches == 0 {
|
||||
let translation = recognizer.velocity(in: recognizer.view)
|
||||
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let recognizer = self.recognizer, otherGestureRecognizer == recognizer {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
/*if gestureRecognizer === self.recognizer && otherGestureRecognizer is InteractiveTransitionGestureRecognizer {
|
||||
return true
|
||||
}*/
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3))
|
||||
self.revealOptionsInteractivelyClosed?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func revealGesture(_ recognizer: SwipeOptionsGestureRecognizer) {
|
||||
guard let (size, _, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let leftRevealView = self.leftRevealView {
|
||||
let revealSize = leftRevealView.bounds.size
|
||||
let location = recognizer.location(in: self)
|
||||
if location.x < revealSize.width {
|
||||
recognizer.becomeCancelled()
|
||||
} else {
|
||||
self.initialRevealOffset = self.revealOffset
|
||||
}
|
||||
} else if let rightRevealView = self.rightRevealView {
|
||||
let revealSize = rightRevealView.bounds.size
|
||||
let location = recognizer.location(in: self)
|
||||
if location.x > size.width - revealSize.width {
|
||||
recognizer.becomeCancelled()
|
||||
} else {
|
||||
self.initialRevealOffset = self.revealOffset
|
||||
}
|
||||
} else {
|
||||
if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty {
|
||||
recognizer.becomeCancelled()
|
||||
}
|
||||
self.initialRevealOffset = self.revealOffset
|
||||
}
|
||||
case .changed:
|
||||
var translation = recognizer.translation(in: self)
|
||||
translation.x += self.initialRevealOffset
|
||||
if self.revealOptions.left.isEmpty {
|
||||
translation.x = min(0.0, translation.x)
|
||||
}
|
||||
if self.leftRevealView == nil && CGFloat(0.0).isLess(than: translation.x) {
|
||||
self.setupAndAddLeftRevealNode()
|
||||
self.revealOptionsInteractivelyOpened?()
|
||||
} else if self.rightRevealView == nil && translation.x.isLess(than: 0.0) {
|
||||
self.setupAndAddRightRevealNode()
|
||||
self.revealOptionsInteractivelyOpened?()
|
||||
}
|
||||
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
|
||||
if self.leftRevealView == nil && self.rightRevealView == nil {
|
||||
self.revealOptionsInteractivelyClosed?()
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
guard let recognizer = self.recognizer else {
|
||||
break
|
||||
}
|
||||
|
||||
if let leftRevealView = self.leftRevealView {
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
let revealSize = leftRevealView.bounds.size
|
||||
var reveal = false
|
||||
if abs(velocity.x) < 100.0 {
|
||||
if self.initialRevealOffset.isZero && self.revealOffset > 0.0 {
|
||||
reveal = true
|
||||
} else if self.revealOffset > revealSize.width {
|
||||
reveal = true
|
||||
} else {
|
||||
reveal = false
|
||||
}
|
||||
} else {
|
||||
if velocity.x > 0.0 {
|
||||
reveal = true
|
||||
} else {
|
||||
reveal = false
|
||||
}
|
||||
}
|
||||
|
||||
var selectedOption: Option?
|
||||
if reveal && leftRevealView.isDisplayingExtendedAction() {
|
||||
reveal = false
|
||||
selectedOption = self.revealOptions.left.first
|
||||
} else {
|
||||
self.updateRevealOffsetInternal(offset: reveal ? revealSize.width : 0.0, transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
if let selectedOption = selectedOption {
|
||||
self.revealOptionSelected?(selectedOption, true)
|
||||
} else {
|
||||
if !reveal {
|
||||
self.revealOptionsInteractivelyClosed?()
|
||||
}
|
||||
}
|
||||
} else if let rightRevealView = self.rightRevealView {
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
let revealSize = rightRevealView.bounds.size
|
||||
var reveal = false
|
||||
if abs(velocity.x) < 100.0 {
|
||||
if self.initialRevealOffset.isZero && self.revealOffset < 0.0 {
|
||||
reveal = true
|
||||
} else if self.revealOffset < -revealSize.width {
|
||||
reveal = true
|
||||
} else {
|
||||
reveal = false
|
||||
}
|
||||
} else {
|
||||
if velocity.x < 0.0 {
|
||||
reveal = true
|
||||
} else {
|
||||
reveal = false
|
||||
}
|
||||
}
|
||||
|
||||
var selectedOption: Option?
|
||||
if reveal && rightRevealView.isDisplayingExtendedAction() {
|
||||
reveal = false
|
||||
selectedOption = self.revealOptions.right.last
|
||||
} else {
|
||||
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
if let selectedOption = selectedOption {
|
||||
self.revealOptionSelected?(selectedOption, true)
|
||||
} else {
|
||||
if !reveal {
|
||||
self.revealOptionsInteractivelyClosed?()
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAndAddLeftRevealNode() {
|
||||
if !self.revealOptions.left.isEmpty {
|
||||
let revealView = OptionsView(optionSelected: { [weak self] option in
|
||||
self?.revealOptionSelected?(option, false)
|
||||
}, tapticAction: { [weak self] in
|
||||
self?.hapticImpact()
|
||||
})
|
||||
revealView.setOptions(self.revealOptions.left, isLeft: true)
|
||||
self.leftRevealView = revealView
|
||||
|
||||
if let (size, leftInset, _) = self.validLayout {
|
||||
var revealSize = revealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
||||
revealSize.width += leftInset
|
||||
|
||||
revealView.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
||||
revealView.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
|
||||
}
|
||||
|
||||
self.controlsContainer.addSubview(revealView)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAndAddRightRevealNode() {
|
||||
if !self.revealOptions.right.isEmpty {
|
||||
let revealView = OptionsView(optionSelected: { [weak self] option in
|
||||
self?.revealOptionSelected?(option, false)
|
||||
}, tapticAction: { [weak self] in
|
||||
self?.hapticImpact()
|
||||
})
|
||||
revealView.setOptions(self.revealOptions.right, isLeft: false)
|
||||
self.rightRevealView = revealView
|
||||
|
||||
if let (size, _, rightInset) = self.validLayout {
|
||||
var revealSize = revealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
||||
revealSize.width += rightInset
|
||||
|
||||
revealView.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
|
||||
revealView.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
|
||||
}
|
||||
|
||||
self.controlsContainer.addSubview(revealView)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.validLayout = (size, leftInset, rightInset)
|
||||
|
||||
if let leftRevealView = self.leftRevealView {
|
||||
var revealSize = leftRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
||||
revealSize.width += leftInset
|
||||
leftRevealView.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
||||
}
|
||||
|
||||
if let rightRevealView = self.rightRevealView {
|
||||
var revealSize = rightRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
||||
revealSize.width += rightInset
|
||||
rightRevealView.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
|
||||
}
|
||||
}
|
||||
|
||||
open func updateRevealOffsetInternal(offset: CGFloat, transition: Transition, completion: (() -> Void)? = nil) {
|
||||
self.revealOffset = offset
|
||||
guard let (size, leftInset, rightInset) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var leftRevealCompleted = true
|
||||
var rightRevealCompleted = true
|
||||
let intermediateCompletion = {
|
||||
if leftRevealCompleted && rightRevealCompleted {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
if let leftRevealView = self.leftRevealView {
|
||||
leftRevealCompleted = false
|
||||
|
||||
let revealSize = leftRevealView.bounds.size
|
||||
|
||||
let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
||||
let revealNodeOffset = -self.revealOffset
|
||||
leftRevealView.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition)
|
||||
|
||||
if CGFloat(offset).isLessThanOrEqualTo(0.0) {
|
||||
self.leftRevealView = nil
|
||||
transition.setFrame(view: leftRevealView, frame: revealFrame, completion: { [weak leftRevealView] _ in
|
||||
leftRevealView?.removeFromSuperview()
|
||||
|
||||
leftRevealCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
} else {
|
||||
transition.setFrame(view: leftRevealView, frame: revealFrame, completion: { _ in
|
||||
leftRevealCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
}
|
||||
}
|
||||
if let rightRevealView = self.rightRevealView {
|
||||
rightRevealCompleted = false
|
||||
|
||||
let revealSize = rightRevealView.bounds.size
|
||||
|
||||
let revealFrame = CGRect(origin: CGPoint(x: min(size.width, size.width + self.revealOffset), y: 0.0), size: revealSize)
|
||||
let revealNodeOffset = -self.revealOffset
|
||||
rightRevealView.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition)
|
||||
|
||||
if CGFloat(0.0).isLessThanOrEqualTo(offset) {
|
||||
self.rightRevealView = nil
|
||||
transition.setFrame(view: rightRevealView, frame: revealFrame, completion: { [weak rightRevealView] _ in
|
||||
rightRevealView?.removeFromSuperview()
|
||||
|
||||
rightRevealCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
} else {
|
||||
transition.setFrame(view: rightRevealView, frame: revealFrame, completion: { _ in
|
||||
rightRevealCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
}
|
||||
}
|
||||
let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero
|
||||
if allowAnyDirection != self.allowAnyDirection {
|
||||
self.allowAnyDirection = allowAnyDirection
|
||||
self.recognizer?.allowAnyDirection = allowAnyDirection
|
||||
self.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
|
||||
}
|
||||
|
||||
self.updateRevealOffset?(offset, transition)
|
||||
}
|
||||
|
||||
open func setRevealOptionsOpened(_ value: Bool, animated: Bool) {
|
||||
if value != !self.revealOffset.isZero {
|
||||
if !self.revealOffset.isZero {
|
||||
self.recognizer?.becomeCancelled()
|
||||
}
|
||||
let transition: Transition
|
||||
if animated {
|
||||
transition = .spring(duration: 0.3)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
if value {
|
||||
if self.rightRevealView == nil {
|
||||
self.setupAndAddRightRevealNode()
|
||||
if let rightRevealView = self.rightRevealView, let validLayout = self.validLayout {
|
||||
let revealSize = rightRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: validLayout.size.height))
|
||||
self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition)
|
||||
}
|
||||
}
|
||||
} else if !self.revealOffset.isZero {
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func animateRevealOptionsFill(completion: (() -> Void)? = nil) {
|
||||
if let validLayout = self.validLayout {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .spring(duration: 0.3), completion: {
|
||||
self.layer.allowsGroupOpacity = false
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open var preventsTouchesToOtherItems: Bool {
|
||||
return self.isDisplayingRevealedOptions
|
||||
}
|
||||
|
||||
open func touchesToOtherItemsPrevented() {
|
||||
if self.isDisplayingRevealedOptions {
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func hapticImpact() {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
}
|
||||
}
|
@ -1329,7 +1329,21 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
},
|
||||
inlineActions: PeerListItemComponent.InlineActionsState(
|
||||
actions: [PeerListItemComponent.InlineAction(
|
||||
id: AnyHashable(0),
|
||||
title: environment.strings.Common_Delete,
|
||||
color: .destructive,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.additionalPeerList.categories.remove(category)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)]
|
||||
)
|
||||
))))
|
||||
}
|
||||
for peer in self.additionalPeerList.peers {
|
||||
@ -1347,7 +1361,21 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
},
|
||||
inlineActions: PeerListItemComponent.InlineActionsState(
|
||||
actions: [PeerListItemComponent.InlineAction(
|
||||
id: AnyHashable(0),
|
||||
title: environment.strings.Common_Delete,
|
||||
color: .destructive,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id })
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)]
|
||||
)
|
||||
))))
|
||||
}
|
||||
|
||||
|
@ -827,7 +827,21 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
},
|
||||
inlineActions: PeerListItemComponent.InlineActionsState(
|
||||
actions: [PeerListItemComponent.InlineAction(
|
||||
id: AnyHashable(0),
|
||||
title: environment.strings.Common_Delete,
|
||||
color: .destructive,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.additionalPeerList.categories.remove(category)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)]
|
||||
)
|
||||
))))
|
||||
}
|
||||
for peer in self.additionalPeerList.peers {
|
||||
@ -845,7 +859,21 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
},
|
||||
inlineActions: PeerListItemComponent.InlineActionsState(
|
||||
actions: [PeerListItemComponent.InlineAction(
|
||||
id: AnyHashable(0),
|
||||
title: environment.strings.Common_Delete,
|
||||
color: .destructive,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id })
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)]
|
||||
)
|
||||
))))
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ swift_library(
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListItemSwipeOptionContainer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
@ -20,6 +21,7 @@ import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import PhotoResources
|
||||
import ListSectionComponent
|
||||
import ListItemSwipeOptionContainer
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
@ -77,6 +79,58 @@ public final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class InlineAction: Equatable {
|
||||
public enum Color: Equatable {
|
||||
case destructive
|
||||
}
|
||||
|
||||
public let id: AnyHashable
|
||||
public let title: String
|
||||
public let color: Color
|
||||
public let action: () -> Void
|
||||
|
||||
public init(id: AnyHashable, title: String, color: Color, action: @escaping () -> Void) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.color = color
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class InlineActionsState: Equatable {
|
||||
public let actions: [InlineAction]
|
||||
|
||||
public init(actions: [InlineAction]) {
|
||||
self.actions = actions
|
||||
}
|
||||
|
||||
public static func ==(lhs: InlineActionsState, rhs: InlineActionsState) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.actions != rhs.actions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class Reaction: Equatable {
|
||||
public let reaction: MessageReaction.Reaction
|
||||
public let file: TelegramMediaFile?
|
||||
@ -131,6 +185,7 @@ public final class PeerListItemComponent: Component {
|
||||
let isEnabled: Bool
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void
|
||||
let inlineActions: InlineActionsState?
|
||||
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||
|
||||
@ -156,6 +211,7 @@ public final class PeerListItemComponent: Component {
|
||||
isEnabled: Bool = true,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void,
|
||||
inlineActions: InlineActionsState? = nil,
|
||||
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||
) {
|
||||
@ -180,6 +236,7 @@ public final class PeerListItemComponent: Component {
|
||||
self.isEnabled = isEnabled
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.inlineActions = inlineActions
|
||||
self.contextAction = contextAction
|
||||
self.openStories = openStories
|
||||
}
|
||||
@ -245,6 +302,9 @@ public final class PeerListItemComponent: Component {
|
||||
if lhs.hasNext != rhs.hasNext {
|
||||
return false
|
||||
}
|
||||
if lhs.inlineActions != rhs.inlineActions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -252,6 +312,8 @@ public final class PeerListItemComponent: Component {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let swipeOptionContainer: ListItemSwipeOptionContainer
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
@ -306,8 +368,11 @@ public final class PeerListItemComponent: Component {
|
||||
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
self.containerButton.layer.anchorPoint = CGPoint()
|
||||
self.containerButton.isExclusiveTouch = true
|
||||
|
||||
self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect())
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = false
|
||||
self.avatarNode.isUserInteractionEnabled = false
|
||||
@ -319,7 +384,9 @@ public final class PeerListItemComponent: Component {
|
||||
self.addSubview(self.extractedContainerView)
|
||||
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||
|
||||
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||
self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer)
|
||||
|
||||
self.swipeOptionContainer.addSubview(self.containerButton)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
@ -368,6 +435,25 @@ public final class PeerListItemComponent: Component {
|
||||
customUpdateIsHighlighted(highlighted)
|
||||
}
|
||||
}
|
||||
|
||||
self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size))
|
||||
}
|
||||
self.swipeOptionContainer.revealOptionSelected = { [weak self] option, animated in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let inlineActions = component.inlineActions else {
|
||||
return
|
||||
}
|
||||
self.swipeOptionContainer.setRevealOptionsOpened(false, animated: animated)
|
||||
if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) {
|
||||
inlineAction.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -1007,10 +1093,39 @@ public final class PeerListItemComponent: Component {
|
||||
self.extractedContainerView.contentRect = resultBounds
|
||||
|
||||
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)
|
||||
|
||||
let swipeOptionContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame)
|
||||
|
||||
transition.setPosition(view: self.containerButton, position: containerFrame.origin)
|
||||
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerFrame.size))
|
||||
|
||||
self.separatorInset = leftInset
|
||||
|
||||
self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0)
|
||||
|
||||
var rightOptions: [ListItemSwipeOptionContainer.Option] = []
|
||||
if let inlineActions = component.inlineActions {
|
||||
rightOptions = inlineActions.actions.map { action in
|
||||
let color: UIColor
|
||||
let textColor: UIColor
|
||||
switch action.color {
|
||||
case .destructive:
|
||||
color = component.theme.list.itemDisclosureActions.destructive.fillColor
|
||||
textColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor
|
||||
}
|
||||
|
||||
return ListItemSwipeOptionContainer.Option(
|
||||
key: action.id,
|
||||
title: action.title,
|
||||
icon: .none,
|
||||
color: color,
|
||||
textColor: textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
self.swipeOptionContainer.setRevealOptions(([], rightOptions))
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +234,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
}
|
||||
|
||||
var shortcuts: Signal<[ShortcutMessageList.Item], NoError> = .single([])
|
||||
if peer is TelegramUser {
|
||||
if let user = peer as? TelegramUser, user.botInfo == nil {
|
||||
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
|
||||
|
||||
shortcuts = context.engine.accountData.shortcutMessageList()
|
||||
|> map { shortcutMessageList -> [ShortcutMessageList.Item] in
|
||||
return shortcutMessageList.items.filter { item in
|
||||
|
Loading…
x
Reference in New Issue
Block a user