mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
[WIP] Custom channel reactions
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import SwitchComponent
|
||||
import EntityKeyboard
|
||||
import AccountContext
|
||||
|
||||
final class EmojiListInputComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let placeholder: String
|
||||
let reactionItems: [EmojiComponentReactionItem]
|
||||
let isInputActive: Bool
|
||||
let activateInput: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
placeholder: String,
|
||||
reactionItems: [EmojiComponentReactionItem],
|
||||
isInputActive: Bool,
|
||||
activateInput: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.placeholder = placeholder
|
||||
self.reactionItems = reactionItems
|
||||
self.isInputActive = isInputActive
|
||||
self.activateInput = activateInput
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiListInputComponent, rhs: EmojiListInputComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.reactionItems != rhs.reactionItems {
|
||||
return false
|
||||
}
|
||||
if lhs.isInputActive != rhs.isInputActive {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: EmojiListInputComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var itemLayers: [Int64: EmojiPagerContentComponent.View.ItemLayer] = [:]
|
||||
private let trailingPlaceholder = ComponentView<Empty>()
|
||||
private let caretIndicator: UIImageView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.caretIndicator = UIImageView()
|
||||
self.caretIndicator.image = generateImage(CGSize(width: 2.0, height: 4.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.width * 0.5).cgPath)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 2).withRenderingMode(.alwaysTemplate)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
|
||||
var tapOnItem = false
|
||||
for (_, itemLayer) in self.itemLayers {
|
||||
if itemLayer.frame.insetBy(dx: -6.0, dy: -6.0).contains(point) {
|
||||
tapOnItem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !tapOnItem {
|
||||
self.component?.activateInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EmojiListInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let verticalInset: CGFloat = 12.0
|
||||
let placeholderSpacing: CGFloat = 6.0
|
||||
|
||||
let minItemSize: CGFloat = 24.0
|
||||
let itemSpacingFactor: CGFloat = 0.15
|
||||
let minSideInset: CGFloat = 12.0
|
||||
|
||||
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||
self.layer.cornerRadius = 12.0
|
||||
|
||||
let maxItemsWidth = availableSize.width - minSideInset * 2.0
|
||||
let itemsPerRow = Int(floor((maxItemsWidth + minItemSize * itemSpacingFactor) / (minItemSize + minItemSize * itemSpacingFactor)))
|
||||
let itemSizePlusSpacing = maxItemsWidth / CGFloat(itemsPerRow)
|
||||
let itemSize = floor(itemSizePlusSpacing * (1.0 - itemSpacingFactor))
|
||||
let itemSpacing = floor(itemSizePlusSpacing * itemSpacingFactor)
|
||||
let sideInset = floor((availableSize.width - (itemSize * CGFloat(itemsPerRow) + itemSpacing * CGFloat(itemsPerRow - 1))) * 0.5)
|
||||
|
||||
let rowCount = (component.reactionItems.count + (itemsPerRow - 1)) / itemsPerRow
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let trailingPlaceholderSize = self.trailingPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: component.theme.list.itemPlaceholderTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
var lastRowItemCount = component.reactionItems.count % itemsPerRow
|
||||
if lastRowItemCount == 0 {
|
||||
lastRowItemCount = itemsPerRow
|
||||
}
|
||||
let trailingLineWidth = sideInset + CGFloat(lastRowItemCount) * (itemSize + itemSpacing) + placeholderSpacing
|
||||
|
||||
var contentHeight: CGFloat = verticalInset * 2.0 + CGFloat(rowCount) * itemSize + CGFloat(max(0, rowCount - 1)) * itemSpacing
|
||||
let trailingPlaceholderFrame: CGRect
|
||||
if availableSize.width - sideInset - trailingLineWidth < trailingPlaceholderSize.width {
|
||||
contentHeight += itemSize + itemSpacing
|
||||
trailingPlaceholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset + CGFloat(rowCount) * (itemSize + itemSpacing) + floor((itemSize - trailingPlaceholderSize.height) * 0.5)), size: trailingPlaceholderSize)
|
||||
} else {
|
||||
trailingPlaceholderFrame = CGRect(origin: CGPoint(x: trailingLineWidth, y: verticalInset + CGFloat(rowCount - 1) * (itemSize + itemSpacing) + floor((itemSize - trailingPlaceholderSize.height) * 0.5)), size: trailingPlaceholderSize)
|
||||
}
|
||||
|
||||
if let trailingPlaceholderView = self.trailingPlaceholder.view {
|
||||
if trailingPlaceholderView.superview == nil {
|
||||
trailingPlaceholderView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(trailingPlaceholderView)
|
||||
self.addSubview(self.caretIndicator)
|
||||
}
|
||||
transition.setPosition(view: trailingPlaceholderView, position: trailingPlaceholderFrame.origin)
|
||||
trailingPlaceholderView.bounds = CGRect(origin: CGPoint(), size: trailingPlaceholderFrame.size)
|
||||
|
||||
self.caretIndicator.tintColor = component.theme.list.itemAccentColor
|
||||
transition.setFrame(view: self.caretIndicator, frame: CGRect(origin: CGPoint(x: trailingPlaceholderFrame.minX, y: trailingPlaceholderFrame.minY + floorToScreenPixels((trailingPlaceholderFrame.height - 22.0) * 0.5)), size: CGSize(width: 2.0, height: 22.0)))
|
||||
self.caretIndicator.isHidden = !component.isInputActive
|
||||
}
|
||||
|
||||
var validIds: [Int64] = []
|
||||
for i in 0 ..< component.reactionItems.count {
|
||||
let item = component.reactionItems[i]
|
||||
let itemKey = item.file.fileId.id
|
||||
validIds.append(itemKey)
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i % itemsPerRow) * (itemSize + itemSpacing), y: verticalInset + CGFloat(i / itemsPerRow) * (itemSize + itemSpacing)), size: CGSize(width: itemSize, height: itemSize))
|
||||
|
||||
var itemTransition = transition
|
||||
var animateIn = false
|
||||
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
|
||||
if let current = self.itemLayers[itemKey] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
animateIn = true
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(
|
||||
file: item.file
|
||||
)
|
||||
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
||||
item: EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: .none
|
||||
),
|
||||
context: component.context,
|
||||
attemptSynchronousLoad: false,
|
||||
content: EmojiPagerContentComponent.ItemContent.animation(animationData),
|
||||
cache: component.context.animationCache,
|
||||
renderer: component.context.animationRenderer,
|
||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
||||
blurredBadgeColor: .clear,
|
||||
accentIconColor: component.theme.list.itemAccentColor,
|
||||
pointSize: CGSize(width: 32.0, height: 32.0),
|
||||
onUpdateDisplayPlaceholder: { _, _ in
|
||||
}
|
||||
)
|
||||
self.itemLayers[itemKey] = itemLayer
|
||||
self.layer.addSublayer(itemLayer)
|
||||
}
|
||||
itemLayer.isVisibleForAnimations = true
|
||||
|
||||
itemTransition.setFrame(layer: itemLayer, frame: itemFrame)
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
var removedIds: [Int64] = []
|
||||
for (key, itemLayer) in self.itemLayers {
|
||||
if !validIds.contains(key) {
|
||||
removedIds.append(key)
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemLayer] _ in
|
||||
itemLayer?.removeFromSuperlayer()
|
||||
})
|
||||
itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
|
||||
} else {
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
for key in removedIds {
|
||||
self.itemLayers.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user