Emoji improvements

This commit is contained in:
Ali 2022-07-20 01:32:47 +02:00
parent 97de870643
commit b56132cbc1
14 changed files with 567 additions and 148 deletions

View File

@ -6,23 +6,56 @@ import ComponentDisplayAdapters
public final class BlurredBackgroundComponent: Component {
public let color: UIColor
public let tintContainerView: UIView?
public init(
color: UIColor
color: UIColor,
tintContainerView: UIView? = nil
) {
self.color = color
self.tintContainerView = tintContainerView
}
public static func ==(lhs: BlurredBackgroundComponent, rhs: BlurredBackgroundComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.tintContainerView !== rhs.tintContainerView {
return false
}
return true
}
public final class View: BlurredBackgroundView {
private var tintMaskView: UIView?
public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if let tintContainerView = component.tintContainerView {
self.updateColor(color: .clear, forceKeepBlur: true, transition: transition.containedViewLayoutTransition)
let tintMaskView: UIView
if let current = self.tintMaskView {
tintMaskView = current
} else {
tintMaskView = UIView()
self.tintMaskView = tintMaskView
self.addSubview(tintMaskView)
}
tintMaskView.backgroundColor = component.color
transition.setFrame(view: tintMaskView, frame: CGRect(origin: CGPoint(), size: availableSize))
if tintMaskView.mask !== tintContainerView {
tintMaskView.mask = tintContainerView
}
} else {
self.updateColor(color: component.color, transition: transition.containedViewLayoutTransition)
if let tintMaskView = self.tintMaskView {
self.tintMaskView = nil
tintMaskView.removeFromSuperview()
}
}
self.update(size: availableSize, transition: transition.containedViewLayoutTransition)
return availableSize
@ -30,7 +63,7 @@ public final class BlurredBackgroundComponent: Component {
}
public func makeView() -> View {
return View(color: .clear, enableBlur: true)
return View(color: nil, enableBlur: true)
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {

View File

@ -62,7 +62,7 @@ private func adjustFrameRate(animation: CAAnimation) {
}
public extension CALayer {
func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
func makeAnimation(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
let components = timingFunction.components(separatedBy: "_")
let damping = Float(components[1]) ?? 100.0
@ -158,7 +158,7 @@ public extension CALayer {
}
}
func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
self.add(animation, forKey: additive ? nil : keyPath)
}

View File

@ -633,7 +633,7 @@ public extension ContainedViewLayoutTransition {
}
case let .animated(duration, curve):
let previousFrame: CGRect
if beginWithCurrentState, let presentation = view.layer.presentation() {
if beginWithCurrentState, (view.layer.animation(forKey: "position") != nil || view.layer.animation(forKey: "bounds") != nil), let presentation = view.layer.presentation() {
previousFrame = presentation.frame
} else {
previousFrame = view.frame
@ -662,7 +662,7 @@ public extension ContainedViewLayoutTransition {
}
case let .animated(duration, curve):
let previousFrame: CGRect
if beginWithCurrentState, let presentation = layer.presentation() {
if beginWithCurrentState, (layer.animation(forKey: "position") != nil || layer.animation(forKey: "bounds") != nil), let presentation = layer.presentation() {
previousFrame = presentation.frame
} else {
previousFrame = layer.frame

View File

@ -287,7 +287,7 @@ public final class NavigationBackgroundNode: ASDisplayNode {
}
open class BlurredBackgroundView: UIView {
private var _color: UIColor
private var _color: UIColor?
private var enableBlur: Bool
@ -304,8 +304,8 @@ open class BlurredBackgroundView: UIView {
}
}
public init(color: UIColor, enableBlur: Bool = true) {
self._color = .clear
public init(color: UIColor?, enableBlur: Bool = true) {
self._color = nil
self.enableBlur = enableBlur
self.backgroundView = UIView()
@ -314,15 +314,17 @@ open class BlurredBackgroundView: UIView {
self.addSubview(self.backgroundView)
if let color = color {
self.updateColor(color: color, transition: .immediate)
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateBackgroundBlur(forceKeepBlur: Bool) {
if self.enableBlur && !sharedIsReduceTransparencyEnabled && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) {
if let color = self._color, self.enableBlur && !sharedIsReduceTransparencyEnabled && ((color.alpha > .ulpOfOne && color.alpha < 0.95) || forceKeepBlur) {
if self.effectView == nil {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
@ -369,16 +371,16 @@ open class BlurredBackgroundView: UIView {
public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) {
let effectiveEnableBlur = enableBlur ?? self.enableBlur
if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur {
if self._color == color && self.enableBlur == effectiveEnableBlur {
return
}
self._color = color
self.enableBlur = effectiveEnableBlur
if sharedIsReduceTransparencyEnabled {
transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color.withAlphaComponent(1.0))
transition.updateBackgroundColor(layer: self.backgroundView.layer, color: color.withAlphaComponent(1.0))
} else {
transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color)
transition.updateBackgroundColor(layer: self.backgroundView.layer, color: color)
}
self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur)

View File

@ -71,7 +71,7 @@ public func adjustSaturationInContext(context: DrawingContext, saturation: CGFlo
vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
}
private func generateGradient(size: CGSize, colors inputColors: [UIColor], positions: [CGPoint], adjustSaturation: CGFloat = 1.0) -> UIImage {
private func generateGradient(size: CGSize, colors inputColors: [UIColor], positions: [CGPoint], adjustSaturation: CGFloat = 1.0) -> (UIImage, String) {
let colors: [UIColor] = inputColors.count == 1 ? [inputColors[0], inputColors[0], inputColors[0]] : inputColors
let width = Int(size.width)
@ -179,7 +179,23 @@ private func generateGradient(size: CGSize, colors inputColors: [UIColor], posit
adjustSaturationInContext(context: context, saturation: adjustSaturation)
}
return context.generateImage()!
var hashString = ""
hashString.append("\(size.width)x\(size.height)")
for color in colors {
hashString.append("_\(color.argb)")
}
for position in positions {
hashString.append("_\(position.x):\(position.y)")
}
hashString.append("_\(adjustSaturation)")
return (context.generateImage()!, hashString)
}
public protocol GradientBackgroundPatternOverlayLayer: CALayer {
var isAnimating: Bool { get set }
func updateCompositionData(size: CGSize, backgroundImage: UIImage, backgroundImageHash: String)
}
public final class GradientBackgroundNode: ASDisplayNode {
@ -218,12 +234,14 @@ public final class GradientBackgroundNode: ASDisplayNode {
public static func generatePreview(size: CGSize, colors: [UIColor]) -> UIImage {
let positions = gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: 0))
return generateGradient(size: size, colors: colors, positions: positions)
return generateGradient(size: size, colors: colors, positions: positions).0
}
private var colors: [UIColor]
private var phase: Int = 0
private var backgroundImageHash: String?
public let contentView: UIImageView
private var validPhase: Int?
private var invalidated: Bool = false
@ -234,7 +252,7 @@ public final class GradientBackgroundNode: ASDisplayNode {
if let current = self._dimmedImage {
return current
} else if let (size, colors, positions) = self.dimmedImageParams {
self._dimmedImage = generateGradient(size: size, colors: colors, positions: positions, adjustSaturation: self.saturation)
self._dimmedImage = generateGradient(size: size, colors: colors, positions: positions, adjustSaturation: self.saturation).0
return self._dimmedImage
} else {
return nil
@ -247,8 +265,12 @@ public final class GradientBackgroundNode: ASDisplayNode {
private let useSharedAnimationPhase: Bool
static var sharedPhase: Int = 0
private var isAnimating: Bool = false
private let saturation: CGFloat
private var patternOverlayLayer: GradientBackgroundPatternOverlayLayer?
public init(colors: [UIColor]? = nil, useSharedAnimationPhase: Bool = false, adjustSaturation: Bool = true) {
self.useSharedAnimationPhase = useSharedAnimationPhase
self.saturation = adjustSaturation ? 1.7 : 1.0
@ -275,6 +297,31 @@ public final class GradientBackgroundNode: ASDisplayNode {
deinit {
}
public func setPatternOverlay(layer: GradientBackgroundPatternOverlayLayer?) {
if self.patternOverlayLayer === layer {
return
}
if let patternOverlayLayer = self.patternOverlayLayer {
if patternOverlayLayer.superlayer == self.layer {
patternOverlayLayer.removeFromSuperlayer()
}
self.patternOverlayLayer = nil
}
self.patternOverlayLayer = layer
if let patternOverlayLayer = self.patternOverlayLayer {
self.layer.addSublayer(patternOverlayLayer)
patternOverlayLayer.isAnimating = self.isAnimating
if let image = self.contentView.image, let backgroundImageHash = self.backgroundImageHash, self.contentView.bounds.width > 1.0, self.contentView.bounds.height > 1.0 {
patternOverlayLayer.updateCompositionData(size: self.contentView.bounds.size, backgroundImage: image, backgroundImageHash: backgroundImageHash)
}
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, extendAnimation: Bool, backwards: Bool, completion: @escaping () -> Void) {
let sizeUpdated = self.validLayout != size
self.validLayout = size
@ -283,6 +330,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
let positions = gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: self.phase % 8))
let previousImage = self.contentView.image
let previousSize = self.contentView.bounds.size
if let validPhase = self.validPhase {
if validPhase != self.phase || self.invalidated {
self.validPhase = self.phase
@ -318,7 +368,7 @@ public final class GradientBackgroundNode: ASDisplayNode {
}
if case let .animated(duration, curve) = transition, duration > 0.001 {
var images: [UIImage] = []
var images: [(UIImage, String)] = []
var dimmedImages: [UIImage] = []
let needDimmedImages = !self.cloneNodes.isEmpty
@ -350,16 +400,17 @@ public final class GradientBackgroundNode: ASDisplayNode {
images.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions))
if needDimmedImages {
dimmedImages.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions, adjustSaturation: self.saturation))
dimmedImages.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions, adjustSaturation: self.saturation).0)
}
}
self.dimmedImageParams = (imageSize, self.colors, gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: self.phase % 8)))
self.contentView.image = images.last
self.contentView.image = images[images.count - 1].0
self.backgroundImageHash = images[images.count - 1].1
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.values = images.map { $0.cgImage! }
animation.values = images.map { $0.0.cgImage! }
animation.duration = duration * UIView.animationDurationFactor()
if backwards || extendAnimation {
animation.calculationMode = .discrete
@ -372,7 +423,19 @@ public final class GradientBackgroundNode: ASDisplayNode {
animation.beginTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.25
}
animation.completion = { _ in
self.isAnimating = true
if let patternOverlayLayer = self.patternOverlayLayer {
patternOverlayLayer.isAnimating = true
}
animation.completion = { [weak self] value in
if let strongSelf = self, value {
strongSelf.isAnimating = false
if let patternOverlayLayer = strongSelf.patternOverlayLayer {
patternOverlayLayer.isAnimating = false
}
}
completion()
}
@ -399,10 +462,11 @@ public final class GradientBackgroundNode: ASDisplayNode {
}
}
} else {
let image = generateGradient(size: imageSize, colors: self.colors, positions: positions)
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
self.contentView.image = image
self.backgroundImageHash = imageHash
let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: self.saturation)
let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: self.saturation).0
self._dimmedImage = dimmedImage
self.dimmedImageParams = (imageSize, self.colors, positions)
@ -412,14 +476,30 @@ public final class GradientBackgroundNode: ASDisplayNode {
completion()
}
} else if sizeUpdated {
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
self.contentView.image = image
self.backgroundImageHash = imageHash
let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: self.saturation).0
self.dimmedImageParams = (imageSize, self.colors, positions)
for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage
}
self.validPhase = self.phase
completion()
} else {
completion()
}
} else if sizeUpdated {
let image = generateGradient(size: imageSize, colors: self.colors, positions: positions)
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
self.contentView.image = image
self.backgroundImageHash = imageHash
let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: self.saturation)
let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: self.saturation).0
self.dimmedImageParams = (imageSize, self.colors, positions)
for cloneNode in self.cloneNodes {
@ -434,6 +514,12 @@ public final class GradientBackgroundNode: ASDisplayNode {
}
transition.updateFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
if self.contentView.image !== previousImage || self.contentView.bounds.size != previousSize {
if let patternOverlayLayer = self.patternOverlayLayer, let imageHash = self.backgroundImageHash, let image = self.contentView.image, self.contentView.bounds.width > 1.0, self.contentView.bounds.height > 1.0 {
patternOverlayLayer.updateCompositionData(size: size, backgroundImage: image, backgroundImageHash: imageHash)
}
}
}
public func updateColors(colors: [UIColor]) {

View File

@ -1522,52 +1522,56 @@ public final class EmojiPagerContentComponent: Component {
if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) {
component.inputInteraction.clearGroup(id)
}
/*for group in component.itemGroups {
if group.groupId == id {
if group.isPremium && !self.expandedPremiumGroups.contains(id) {
if self.expandedPremiumGroups.contains(id) {
self.expandedPremiumGroups.remove(id)
} else {
self.expandedPremiumGroups.insert(id)
}
let previousItemLayout = self.itemLayout
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
self.state?.updated(transition: transition)
if let previousItemLayout = previousItemLayout, let itemLayout = self.itemLayout {
let boundsOffset = itemLayout.contentSize.height - previousItemLayout.contentSize.height
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentOffset.y + boundsOffset), animated: false)
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -boundsOffset), to: CGPoint(), additive: true)
}
return
} else {
break outer
}
}
}*/
}
}
var foundExactItem = false
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] {
foundExactItem = true
if !itemLayer.displayPlaceholder {
component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer)
}
}
if !foundExactItem {
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] {
if !itemLayer.displayPlaceholder {
component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer)
}
}
}
}
}
private func item(atPoint point: CGPoint) -> (Item, ItemLayer.Key)? {
private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (Item, ItemLayer.Key)? {
let localPoint = self.convert(point, to: self.scrollView)
var closestItem: (key: ItemLayer.Key, distance: CGFloat)?
for (key, itemLayer) in self.visibleItemLayers {
if extendedHitRange {
let position = CGPoint(x: itemLayer.frame.midX, y: itemLayer.frame.midY)
let distance = CGPoint(x: localPoint.x - position.x, y: localPoint.y - position.y)
let distance2 = distance.x * distance.x + distance.y * distance.y
if let closestItemValue = closestItem {
if closestItemValue.distance > distance2 {
closestItem = (key, distance2)
}
} else {
closestItem = (key, distance2)
}
} else {
if itemLayer.frame.contains(localPoint) {
return (itemLayer.item, key)
}
}
}
if let key = closestItem?.key {
if let itemLayer = self.visibleItemLayers[key] {
return (itemLayer.item, key)
}
}
return nil
}
@ -1724,7 +1728,7 @@ public final class EmojiPagerContentComponent: Component {
let groupBorderRadius: CGFloat = 16.0
if itemGroup.isPremiumLocked {
if itemGroup.isPremiumLocked && !itemGroup.isFeatured {
validGroupBorderIds.insert(itemGroup.groupId)
let groupBorderLayer: GroupBorderLayer
var groupBorderTransition = transition

View File

@ -298,6 +298,7 @@ public final class EntityKeyboardComponent: Component {
if let stickerContent = component.stickerContent {
var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in stickerContent.itemGroups {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: String] = [

View File

@ -1063,6 +1063,10 @@ final class EntityKeyboardTopPanelComponent: Component {
private var currentReorderingItemId: AnyHashable?
private var currentReorderingItemContainerView: UIView?
private var initialReorderingItemFrame: CGRect?
private var currentReorderingScrollDisplayLink: ConstantDisplayLinkAnimator?
private lazy var reorderingHapticFeedback: HapticFeedback = {
return HapticFeedback()
}()
private var itemLayout: ItemLayout?
private var items: [Item] = []
@ -1164,7 +1168,11 @@ final class EntityKeyboardTopPanelComponent: Component {
guard let strongSelf = self else {
return
}
let wasReordering = strongSelf.isReordering
strongSelf.updateIsReordering(isActive)
if !isActive, wasReordering {
strongSelf.endReordering()
}
}
)
self.reorderGestureRecognizer = reorderGestureRecognizer
@ -1327,6 +1335,18 @@ final class EntityKeyboardTopPanelComponent: Component {
if let componentView = itemView.componentView {
reorderingItemContainerView.addSubview(componentView)
}
self.reorderingHapticFeedback.impact()
if self.currentReorderingScrollDisplayLink == nil {
self.currentReorderingScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateReorderingAutoscroll()
})
self.currentReorderingScrollDisplayLink?.isPaused = false
}
}
private func endReordering() {
@ -1348,6 +1368,11 @@ final class EntityKeyboardTopPanelComponent: Component {
reorderingItemContainerView.removeFromSuperview()
}
if let currentReorderingScrollDisplayLink = self.currentReorderingScrollDisplayLink {
self.currentReorderingScrollDisplayLink = nil
currentReorderingScrollDisplayLink.invalidate()
}
self.currentReorderingItemId = nil
self.temporaryReorderingOrderIndex = nil
@ -1361,15 +1386,23 @@ final class EntityKeyboardTopPanelComponent: Component {
}
reorderingItemContainerView.frame = initialReorderingItemFrame.offsetBy(dx: offset, dy: 0.0)
let localReorderingItemFrame = reorderingItemContainerView.convert(reorderingItemContainerView.bounds, to: self.scrollView)
for i in 0 ..< self.items.count {
if !self.items[i].isReorderable {
continue
}
let containerFrame = itemLayout.containerFrame(at: i)
if containerFrame.intersects(reorderingItemContainerView.frame) {
if containerFrame.intersects(localReorderingItemFrame) {
let temporaryReorderingOrderIndex: (id: AnyHashable, index: Int) = (currentReorderingItemId, i)
let hadPrevous = self.temporaryReorderingOrderIndex != nil
if self.temporaryReorderingOrderIndex?.id != temporaryReorderingOrderIndex.id || self.temporaryReorderingOrderIndex?.index != temporaryReorderingOrderIndex.index {
self.temporaryReorderingOrderIndex = temporaryReorderingOrderIndex
if hadPrevous {
self.reorderingHapticFeedback.tap()
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
break
@ -1377,6 +1410,34 @@ final class EntityKeyboardTopPanelComponent: Component {
}
}
private func updateReorderingAutoscroll() {
guard let reorderingItemContainerView = self.currentReorderingItemContainerView, let initialReorderingItemFrame = self.initialReorderingItemFrame else {
return
}
var bounds = self.scrollView.bounds
let delta: CGFloat = 3.0
if reorderingItemContainerView.frame.minX < 16.0 {
bounds.origin.x -= delta
} else if reorderingItemContainerView.frame.maxX > self.scrollView.bounds.width - 16.0 {
bounds.origin.x += delta
}
if bounds.origin.x + bounds.size.width > self.scrollView.contentSize.width {
bounds.origin.x = self.scrollView.contentSize.width - bounds.size.width
}
if bounds.origin.x < 0.0 {
bounds.origin.x = 0.0
}
if self.scrollView.bounds != bounds {
self.scrollView.bounds = bounds
let offset = reorderingItemContainerView.frame.minX - initialReorderingItemFrame.minX
self.updateReordering(offset: offset)
}
}
private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) {
guard let itemLayout = self.itemLayout else {
return
@ -1708,6 +1769,16 @@ final class EntityKeyboardTopPanelComponent: Component {
let _ = itemLayout
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
if let component = self.component, let itemLayout = self.itemLayout {
for i in 0 ..< component.items.count {
if component.items[i].id == itemId {
let itemFrame = itemLayout.containerFrame(at: i)
self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -2.0, dy: 0.0), animated: true)
break
}
}
}
/*var found = false
for i in 0 ..< self.items.count {
if self.items[i].id == itemId {

View File

@ -236,7 +236,8 @@ public final class TextNodeWithEntities {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: floor(itemSize * 1.2), height: floor(itemSize * 1.2)))
let pointSize = floor(itemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize))
self.inlineStickerItemLayers[id] = itemLayer
self.textNode.layer.addSublayer(itemLayer)
@ -399,7 +400,8 @@ public class ImmediateTextNodeWithEntities: TextNode {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
let pointSize = floor(itemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize))
self.inlineStickerItemLayers[id] = itemLayer
self.layer.addSublayer(itemLayer)

View File

@ -496,9 +496,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
hasPremium
hasPremium,
context.account.viewTracker.featuredStickerPacks()
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
|> map { view, hasPremium, featuredStickerPacks -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
@ -630,6 +631,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in view.collectionInfos {
installedCollectionIds.insert(id)
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
@ -656,6 +662,33 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
for featuredStickerPack in featuredStickerPacks {
if installedCollectionIds.contains(featuredStickerPack.info.id) {
continue
}
for item in featuredStickerPack.topItems {
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = featuredStickerPack.info.id
let groupId: AnyHashable = supergroupId
let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium
if isPremiumLocked && isPremiumDisabled {
continue
}
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, items: [resultItem]))
}
}
}
return EmojiPagerContentComponent(
id: "stickers",
context: context,

View File

@ -135,7 +135,9 @@ func chatHistoryEntriesForView(
}
if presentationData.largeEmoji, message.media.isEmpty {
if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) {
if stickersEnabled && message.text.count == 1, let entities = message.textEntitiesAttribute?.entities, entities.count == 1, case let .CustomEmoji(_, fileId) = entities[0].type, let _ = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] {
contentTypeHint = .animatedEmoji
} else if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) {
contentTypeHint = .animatedEmoji
} else if message.text.count < 10 && messageIsElligibleForLargeEmoji(message) {
contentTypeHint = .largeEmoji

View File

@ -518,9 +518,17 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
} else if self.telegramFile == nil && self.telegramDice == nil {
let (emoji, fitz) = item.message.text.basicEmoji
var emojiFile: TelegramMediaFile?
var emojiFile: TelegramMediaFile?
if let entities = item.message.textEntitiesAttribute?.entities, entities.count == 1, case let .CustomEmoji(_, fileId) = entities[0].type {
if let file = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
emojiFile = file
}
}
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
}
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
}
@ -547,11 +555,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
var animationItems: [Int: StickerPackItem]?
if let emojiFile = emojiFile, emojiFile.isCustomEmoji {
} else {
if let items = item.associatedData.additionalAnimatedEmojiStickers[textEmoji] {
animationItems = items
} else if let items = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] {
animationItems = items
}
}
if let animationItems = animationItems {
for (_, animationItem) in animationItems {
@ -580,18 +591,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
playbackMode = .once
}
} else if let emojiFile = self.emojiFile {
isEmoji = true
file = emojiFile
//if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
if emojiFile.isCustomEmoji {
playbackMode = .loop
} else {
isEmoji = true
playbackMode = .still(.end)
//} else {
// playbackMode = .once
//}
let (_, fitz) = item.message.text.basicEmoji
if let fitz = fitz {
fitzModifier = EmojiFitzModifier(emoji: fitz)
}
}
}
let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations
if !isPlaying {
@ -1805,7 +1818,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return .optionalAction({
item.controllerInteraction.displayDiceTooltip(dice)
})
} else if let _ = self.emojiFile {
} else if let emojiFile = self.emojiFile, !emojiFile.isCustomEmoji {
if let animationNode = self.animationNode as? AnimatedStickerNode, let _ = recognizer {
var shouldPlay = false
if !animationNode.isPlaying {

View File

@ -809,7 +809,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return UIView()
}
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: 24.0, height: 24.0))
let pointSize = floor(24.0 * 1.3)
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
}
}
}

View File

@ -64,6 +64,208 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
func makeDimmedNode() -> ASDisplayNode?
}
private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer {
enum SoftlightMode {
case whileAnimating
case always
case never
}
var patternContentImage: UIImage? {
didSet {
if self.patternContentImage !== oldValue {
self.updateComposedImage()
self.updateContents()
}
}
}
var composedContentImage: UIImage? {
didSet {
if self.composedContentImage !== oldValue {
self.updateContents()
}
}
}
var softlightMode: SoftlightMode = .whileAnimating {
didSet {
if self.softlightMode != oldValue {
self.updateFilters()
}
}
}
var isAnimating: Bool = false {
didSet {
if self.isAnimating != oldValue {
self.updateFilters()
}
}
}
private var isUsingSoftlight: Bool = false
var suspendCompositionUpdates: Bool = false
private var needsCompositionUpdate: Bool = false
private func updateFilters() {
let useSoftlight: Bool
switch self.softlightMode {
case .whileAnimating:
useSoftlight = self.isAnimating
case .always:
useSoftlight = true
case .never:
useSoftlight = false
}
if self.isUsingSoftlight != useSoftlight {
self.isUsingSoftlight = useSoftlight
if self.isUsingSoftlight {
self.compositingFilter = "softLightBlendMode"
} else {
self.compositingFilter = nil
}
self.updateContents()
self.updateOpacity()
}
}
private var allowSettingContents: Bool = false
private var currentContents: UIImage?
override var contents: Any? {
get {
return super.contents
} set(value) {
if self.allowSettingContents {
super.contents = value
} else {
assert(false)
}
}
}
private var allowSettingOpacity: Bool = false
var compositionOpacity: Float = 1.0 {
didSet {
if self.compositionOpacity != oldValue {
self.updateOpacity()
self.updateComposedImage()
}
}
}
override var opacity: Float {
get {
return super.opacity
} set(value) {
if self.allowSettingOpacity {
super.opacity = value
} else {
assert(false)
}
}
}
private var compositionData: (size: CGSize, backgroundImage: UIImage, backgroundImageHash: String)?
func updateCompositionData(size: CGSize, backgroundImage: UIImage, backgroundImageHash: String) {
if self.compositionData?.size == size && self.compositionData?.backgroundImage === backgroundImage {
return
}
self.compositionData = (size, backgroundImage, backgroundImageHash)
self.updateComposedImage()
}
func updateCompositionIfNeeded() {
if self.needsCompositionUpdate {
self.needsCompositionUpdate = false
self.updateComposedImage()
}
}
private static var cachedComposedImage: (size: CGSize, patternContentImage: UIImage, backgroundImageHash: String, image: UIImage)?
private func updateComposedImage() {
if self.suspendCompositionUpdates {
self.needsCompositionUpdate = true
return
}
switch self.softlightMode {
case .always:
return
default:
break
}
guard let (size, backgroundImage, backgroundImageHash) = self.compositionData, let patternContentImage = self.patternContentImage else {
return
}
if let cachedComposedImage = EffectImageLayer.cachedComposedImage, cachedComposedImage.size == size, cachedComposedImage.backgroundImageHash == backgroundImageHash, cachedComposedImage.patternContentImage === patternContentImage {
self.composedContentImage = cachedComposedImage.image
return
}
#if DEBUG
let startTime = CFAbsoluteTimeGetCurrent()
#endif
let composedContentImage = generateImage(size, contextGenerator: { size, context in
context.draw(backgroundImage.cgImage!, in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.softLight)
context.setAlpha(CGFloat(self.compositionOpacity))
context.draw(patternContentImage.cgImage!, in: CGRect(origin: CGPoint(), size: size))
}, opaque: true, scale: min(UIScreenScale, patternContentImage.scale))
self.composedContentImage = composedContentImage
#if DEBUG
print("Wallpaper composed image updated in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
#endif
if self.softlightMode == .whileAnimating, let composedContentImage = composedContentImage {
EffectImageLayer.cachedComposedImage = (size, patternContentImage, backgroundImageHash, composedContentImage)
}
}
private func updateContents() {
var contents: UIImage?
if self.isUsingSoftlight {
contents = self.patternContentImage
} else {
contents = self.composedContentImage
}
if self.currentContents !== contents {
self.currentContents = contents
self.allowSettingContents = true
self.contents = contents?.cgImage
self.allowSettingContents = false
}
}
private func updateOpacity() {
if self.isUsingSoftlight {
self.allowSettingOpacity = true
self.opacity = self.compositionOpacity
self.allowSettingOpacity = false
self.isOpaque = false
} else {
self.allowSettingOpacity = true
self.opacity = 1.0
self.allowSettingOpacity = false
self.isOpaque = true
}
}
}
final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode {
final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
private let bubbleType: WallpaperBubbleType
@ -348,7 +550,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
private var gradientBackgroundNode: GradientBackgroundNode?
private var outgoingBubbleGradientBackgroundNode: GradientBackgroundNode?
private let patternImageNode: ASImageNode
private let patternImageLayer: EffectImageLayer
private var isGeneratingPatternImage: Bool = false
private let bakedBackgroundView: UIImageView
@ -466,7 +668,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.contentNode = ASDisplayNode()
self.contentNode.contentMode = self.imageContentMode
self.patternImageNode = ASImageNode()
self.patternImageLayer = EffectImageLayer()
self.bakedBackgroundView = UIImageView()
self.bakedBackgroundView.isHidden = true
@ -476,49 +678,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.clipsToBounds = true
self.contentNode.frame = self.bounds
self.addSubnode(self.contentNode)
self.addSubnode(self.patternImageNode)
/*let animationList: [(String, CGPoint)] = [
("ptrnCAT_1162_1918", CGPoint(x: 1162 - 256, y: 1918 - 256)),
("ptrnDOG_0440_2284", CGPoint(x: 440 - 256, y: 2284 - 256)),
("ptrnGLOB_0438_1553", CGPoint(x: 438 - 256, y: 1553 - 256)),
("ptrnSLON_0906_1033", CGPoint(x: 906 - 256, y: 1033 - 256))
]
for (animation, relativePosition) in animationList {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.automaticallyLoadFirstFrame = true
animationNode.autoplay = true
//self.inlineAnimationNodes.append((animationNode, relativePosition))
self.patternImageNode.addSubnode(animationNode)
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animation), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
}
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
for (animationNode, _) in strongSelf.inlineAnimationNodes {
animationNode.visibility = true
}
strongSelf.activateInlineAnimationTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: {
guard let strongSelf = self else {
return
}
strongSelf.inlineAnimationNodes[Int.random(in: 0 ..< strongSelf.inlineAnimationNodes.count)].0.play()
}, queue: .mainQueue())
strongSelf.activateInlineAnimationTimer?.start()
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
for (animationNode, _) in strongSelf.inlineAnimationNodes {
animationNode.visibility = false
}
strongSelf.activateInlineAnimationTimer?.invalidate()
}*/
self.layer.addSublayer(self.patternImageLayer)
}
deinit {
@ -556,7 +716,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
let gradientBackgroundNode = createGradientBackgroundNode(colors: mappedColors, useSharedAnimationPhase: self.useSharedAnimationPhase)
self.gradientBackgroundNode = gradientBackgroundNode
self.insertSubnode(gradientBackgroundNode, aboveSubnode: self.contentNode)
gradientBackgroundNode.addSubnode(self.patternImageNode)
gradientBackgroundNode.setPatternOverlay(layer: self.patternImageLayer)
}
self.gradientBackgroundNode?.updateColors(colors: mappedColors)
@ -569,7 +729,8 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
if let gradientBackgroundNode = self.gradientBackgroundNode {
self.gradientBackgroundNode = nil
gradientBackgroundNode.removeFromSupernode()
self.insertSubnode(self.patternImageNode, aboveSubnode: self.contentNode)
gradientBackgroundNode.setPatternOverlay(layer: nil)
self.layer.insertSublayer(self.patternImageLayer, above: self.contentNode.layer)
}
self.motionEnabled = wallpaper.settings?.motion ?? false
@ -654,40 +815,44 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
let intensity = CGFloat(file.settings.intensity ?? 50) / 100.0
if intensity < 0 {
self.patternImageNode.alpha = 1.0
self.patternImageNode.layer.compositingFilter = nil
self.patternImageLayer.compositionOpacity = 1.0
self.patternImageLayer.softlightMode = .never
} else {
self.patternImageNode.alpha = intensity
self.patternImageLayer.compositionOpacity = Float(intensity)
if patternIsBlack {
self.patternImageNode.layer.compositingFilter = nil
self.patternImageLayer.softlightMode = .never
} else {
self.patternImageNode.layer.compositingFilter = "softLightBlendMode"
if self.useSharedAnimationPhase {
self.patternImageLayer.softlightMode = .whileAnimating
} else {
self.patternImageLayer.softlightMode = .always
}
}
}
self.patternImageNode.isHidden = false
self.patternImageLayer.isHidden = false
let invertPattern = intensity < 0
if invertPattern {
self.backgroundColor = .black
let contentAlpha = abs(intensity)
self.gradientBackgroundNode?.contentView.alpha = contentAlpha
self.contentNode.alpha = contentAlpha
if self.patternImageNode.image != nil {
self.patternImageNode.backgroundColor = nil
if self.patternImageLayer.contents != nil {
self.patternImageLayer.backgroundColor = nil
} else {
self.patternImageNode.backgroundColor = .black
self.patternImageLayer.backgroundColor = UIColor.black.cgColor
}
} else {
self.backgroundColor = nil
self.gradientBackgroundNode?.contentView.alpha = 1.0
self.contentNode.alpha = 1.0
self.patternImageNode.backgroundColor = nil
self.patternImageLayer.backgroundColor = nil
}
default:
self.patternImageDisposable.set(nil)
self.validPatternImage = nil
self.patternImageNode.isHidden = true
self.patternImageNode.backgroundColor = nil
self.patternImageLayer.isHidden = true
self.patternImageLayer.backgroundColor = nil
self.backgroundColor = nil
self.gradientBackgroundNode?.contentView.alpha = 1.0
self.contentNode.alpha = 1.0
@ -784,10 +949,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
if invertPattern {
patternColor = .clear
patternBackgroundColor = .clear
if self.patternImageNode.image == nil {
self.patternImageNode.backgroundColor = .black
if self.patternImageLayer.contents == nil {
self.patternImageLayer.backgroundColor = UIColor.black.cgColor
} else {
self.patternImageNode.backgroundColor = nil
self.patternImageLayer.backgroundColor = nil
}
} else {
if patternIsLight {
@ -796,7 +961,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
patternColor = .white
}
patternBackgroundColor = .clear
self.patternImageNode.backgroundColor = nil
self.patternImageLayer.backgroundColor = nil
}
let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern)
@ -805,15 +970,21 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.validPatternGeneratedImage = updatedGeneratedImage
if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated == updatedGeneratedImage {
self.patternImageNode.image = cachedValidPatternImage.image
self.patternImageLayer.suspendCompositionUpdates = true
self.updatePatternPresentation()
self.patternImageLayer.patternContentImage = cachedValidPatternImage.image
self.patternImageLayer.suspendCompositionUpdates = false
self.patternImageLayer.updateCompositionIfNeeded()
} else {
let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false), scale: min(2.0, UIScreenScale))
if self.useSharedAnimationPhase || self.patternImageNode.image == nil {
if self.useSharedAnimationPhase || self.patternImageLayer.contents == nil {
if let drawingContext = validPatternImage.generate(patternArguments) {
if let image = drawingContext.generateImage() {
self.patternImageNode.image = image
self.patternImageLayer.suspendCompositionUpdates = true
self.updatePatternPresentation()
self.patternImageLayer.patternContentImage = image
self.patternImageLayer.suspendCompositionUpdates = false
self.patternImageLayer.updateCompositionIfNeeded()
if self.useSharedAnimationPhase {
WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image)
@ -833,7 +1004,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
return
}
strongSelf.isGeneratingPatternImage = false
strongSelf.patternImageNode.image = image
strongSelf.patternImageLayer.patternContentImage = image
strongSelf.updatePatternPresentation()
if let image = image, strongSelf.useSharedAnimationPhase {
@ -856,7 +1027,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
}
transition.updateFrame(node: self.patternImageNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size))
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {