Business fixes

This commit is contained in:
Isaac 2024-03-01 18:32:29 +04:00
parent addc4dc436
commit 9ce2c2dc9e
10 changed files with 1127 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/TextFormat",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListItemSwipeOptionContainer",
],
visibility = [
"//visibility:public",

View File

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

View File

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