Various fixes

This commit is contained in:
Ilya Laktyushin 2022-07-14 20:46:23 +02:00
commit 3fed69ee0f
124 changed files with 3573 additions and 618 deletions

View file

@ -7838,6 +7838,12 @@ Sorry for the inconvenience.";
"Notification.PremiumGift.Subtitle" = "for %@";
"Notification.PremiumGift.View" = "View";
"StickerPack.AddEmojiCount_1" = "Add 1 Emoji";
"StickerPack.AddEmojiCount_any" = "Add %@ Emoji";
"StickerPack.RemoveEmojiCount_1" = "Remove 1 Emoji";
"StickerPack.RemoveEmojiCount_any" = "Remove %@ Emoji";
"Gallery.AirPlay" = "AirPlay";
"Gallery.AirPlayPlaceholder" = "This video is playing on the TV using AirPlay";

View file

@ -260,6 +260,11 @@ public struct ResolvedBotAdminRights: OptionSet {
}
}
public enum StickerPackUrlType {
case stickers
case emoji
}
public enum ResolvedUrl {
case externalUrl(String)
case urlAuth(String)
@ -269,7 +274,7 @@ public enum ResolvedUrl {
case groupBotStart(peerId: PeerId, payload: String, adminRights: ResolvedBotAdminRights?)
case channelMessage(peerId: PeerId, messageId: MessageId, timecode: Double?)
case replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage, messageId: MessageId)
case stickerPack(name: String)
case stickerPack(name: String, type: StickerPackUrlType)
case instantView(TelegramMediaWebpage, String?)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
case join(String)

View file

@ -18,7 +18,7 @@ public func textAlertController(alertContext: AlertControllerContext, title: Str
let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in
controller?.theme = theme
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
@ -44,7 +44,7 @@ public func richTextAlertController(alertContext: AlertControllerContext, title:
let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in
controller?.theme = theme
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}

View file

@ -304,7 +304,7 @@ public func archivedStickerPacksNoticeController(context: AccountContext, archiv
let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
disposable.dispose()
}

View file

@ -445,7 +445,7 @@ func tipEditController(sharedContext: SharedAccountContext, account: Account, fo
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in

View file

@ -91,7 +91,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -1412,7 +1412,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -1801,7 +1801,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
context: item.context,
cache: item.interaction.animationCache,
renderer: item.interaction.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: synchronousLoads
))
let _ = authorApply()

View file

@ -420,7 +420,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in

View file

@ -53,7 +53,7 @@ private func makeSpringAnimation(keyPath: String) -> CASpringAnimation {
}
private extension CALayer {
func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
func makeAnimation(from: AnyObject?, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
switch curve {
case .spring:
let animation = makeSpringAnimation(keyPath: keyPath)
@ -88,12 +88,14 @@ private extension CALayer {
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
if let from = from {
animation.fromValue = from
}
animation.toValue = to
animation.duration = duration
animation.timingFunction = curve.asTimingFunction()
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
animation.fillMode = .both
animation.speed = speed
animation.isAdditive = additive
if let completion = completion {
@ -225,6 +227,36 @@ public struct Transition {
}
}
public func setFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
if layer.frame == frame {
completion?(true)
return
}
switch self.animation {
case .none:
layer.frame = frame
//view.bounds = CGRect(origin: view.bounds.origin, size: frame.size)
//view.layer.position = CGPoint(x: frame.midX, y: frame.midY)
layer.removeAnimation(forKey: "position")
layer.removeAnimation(forKey: "bounds")
completion?(true)
case .curve:
let previousFrame: CGRect
if (layer.animation(forKey: "position") != nil || layer.animation(forKey: "bounds") != nil), let presentation = layer.presentation() {
previousFrame = presentation.frame
} else {
previousFrame = layer.frame
}
layer.frame = frame
//view.bounds = CGRect(origin: previousBounds.origin, size: frame.size)
//view.center = CGPoint(x: frame.midX, y: frame.midY)
self.animatePosition(layer: layer, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion)
self.animateBounds(layer: layer, from: CGRect(origin: layer.bounds.origin, size: previousFrame.size), to: CGRect(origin: layer.bounds.origin, size: frame.size))
}
}
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
if view.bounds == bounds {
completion?(true)
@ -454,6 +486,10 @@ public struct Transition {
self.animateBounds(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion)
}
public func animateBoundsOrigin(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animateBoundsOrigin(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion)
}
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
@ -491,4 +527,102 @@ public struct Transition {
)
}
}
public func animateBoundsOrigin(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
break
case let .curve(duration, curve):
layer.animate(
from: NSValue(cgPoint: fromValue),
to: NSValue(cgPoint: toValue),
keyPath: "bounds.origin",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: additive,
completion: completion
)
}
}
public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
if layer.cornerRadius == cornerRadius {
return
}
switch self.animation {
case .none:
layer.cornerRadius = cornerRadius
completion?(true)
case let .curve(duration, curve):
let fromValue: CGFloat
if layer.animation(forKey: "cornerRadius") != nil, let presentation = layer.presentation() {
fromValue = presentation.cornerRadius
} else {
fromValue = layer.cornerRadius
}
layer.cornerRadius = cornerRadius
layer.animate(
from: fromValue as NSNumber,
to: cornerRadius as NSNumber,
keyPath: "cornerRadius",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
}
}
public func setShapeLayerPath(layer: CAShapeLayer, path: CGPath, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
layer.path = path
case let .curve(duration, curve):
if let previousPath = layer.path {
layer.animate(
from: previousPath,
to: path,
keyPath: "path",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
layer.path = path
} else {
layer.path = path
}
}
}
public func setShapeLayerLineDashPattern(layer: CAShapeLayer, pattern: [NSNumber], completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
layer.lineDashPattern = pattern
case let .curve(duration, curve):
if let previousLineDashPattern = layer.lineDashPattern {
layer.lineDashPattern = pattern
layer.animate(
from: previousLineDashPattern as CFArray,
to: pattern as CFArray,
keyPath: "lineDashPattern",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
} else {
layer.lineDashPattern = pattern
}
}
}
}

View file

@ -154,7 +154,7 @@ public final class Button: Component {
self.holdActionTimer?.invalidate()
if #available(iOS 10.0, *) {
let holdActionTimer = Timer(timeInterval: 1.0, repeats: false, block: { [weak self] _ in
let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
@ -173,7 +173,7 @@ public final class Button: Component {
private func beginExecuteHoldActionTimer() {
self.holdActionTimer?.invalidate()
if #available(iOS 10.0, *) {
let holdActionTimer = Timer(timeInterval: 0.2, repeats: true, block: { [weak self] _ in
let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in
guard let strongSelf = self else {
return
}

View file

@ -203,9 +203,6 @@ public final class ComponentView<EnvironmentType> {
}
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)
if transition.userData(ComponentHostViewSkipSettingFrame.self) == nil {
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize))
}
if isEnvironmentUpdated {
context.erasedEnvironment._isUpdated = false

View file

@ -7,8 +7,13 @@ import Display
public final class LottieAnimationComponent: Component {
public struct AnimationItem: Equatable {
public enum StillPosition {
case begin
case end
}
public enum Mode: Equatable {
case still
case still(position: StillPosition)
case animating(loop: Bool)
case animateTransitionFromPrevious
}
@ -153,7 +158,11 @@ public final class LottieAnimationComponent: Component {
view.backgroundColor = .clear
view.isOpaque = false
//view.logHierarchyKeypaths()
if let value = component.animation.colors["__allcolors__"] {
for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
for (key, value) in component.animation.colors {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
@ -188,6 +197,14 @@ public final class LottieAnimationComponent: Component {
}
}
} else {
if case let .still(position) = component.animation.mode {
switch position {
case .begin:
animationView.currentFrame = 0.0
case .end:
animationView.currentFrame = animationView.animation?.endFrame ?? 0.0
}
}
if animationView.isAnimationPlaying {
animationView.stop()
}

View file

@ -298,6 +298,8 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
if let paneTransitionGestureState = self.paneTransitionGestureState {
self.paneTransitionGestureState = nil
var updateTopPanelExpanded = false
if paneTransitionGestureState.fraction != 0.0, let component = self.component, let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) {
let fraction = recognizer.translation(in: self).x / self.bounds.width
let velocity = recognizer.velocity(in: self)
@ -318,10 +320,18 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
}
if updatedCentralIndex != centralIndex {
self.centralId = component.contents[updatedCentralIndex].id
if self.isTopPanelExpanded {
updateTopPanelExpanded = true
}
}
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
if updateTopPanelExpanded {
self.isTopPanelExpandedUpdated(isExpanded: false, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
}
default:
break
@ -343,8 +353,19 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
guard let strongSelf = self else {
return
}
var updateTopPanelExpanded = false
if strongSelf.centralId != id {
strongSelf.centralId = id
if strongSelf.isTopPanelExpanded {
updateTopPanelExpanded = true
}
}
if updateTopPanelExpanded {
strongSelf.isTopPanelExpandedUpdated(isExpanded: false, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
}

View file

@ -30,7 +30,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -666,8 +666,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}*/
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
let contentParentNode = ContextControllerContentNode(sourceNode: sourceNode, controller: source.controller, tapped: { [weak self] in
if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() {
let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in
self?.attemptTransitionControllerIntoNavigation()
})
self.contentContainerNode.contentNode = .controller(contentParentNode)
@ -676,7 +676,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentContainerNode.cornerRadius = 14.0
self.contentContainerNode.addSubnode(contentParentNode)
let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view)
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
}
@ -713,8 +713,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view)
if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() {
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
@ -857,7 +857,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode {
let snapshotView: UIView? = nil// controller.sourceNode.view.snapshotContentTree()
if let snapshotView = snapshotView {
controller.sourceNode.isHidden = true
controller.sourceView.isHidden = true
self.view.insertSubview(snapshotView, belowSubview: self.contentContainerNode.view)
snapshotView.layer.animateSpring(from: NSValue(cgPoint: localSourceFrame.center), to: NSValue(cgPoint: CGPoint(x: self.contentContainerNode.frame.midX, y: self.contentContainerNode.frame.minY + localSourceFrame.height / 2.0)), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false)
@ -1166,8 +1166,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var completedContentNode = false
var completedActionsNode = false
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view)
if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() {
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
@ -1251,13 +1251,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { [weak self] _ in
completedContentNode = true
if let strongSelf = self, let contentNode = strongSelf.contentContainerNode.contentNode, case let .controller(controller) = contentNode {
controller.sourceNode.isHidden = false
controller.sourceView.isHidden = false
}
intermediateCompletion()
})
} else {
if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode {
controller.sourceNode.isHidden = false
controller.sourceView.isHidden = false
}
if let snapshotView = controller.view.snapshotContentTree(keepTransform: true) {
@ -1768,12 +1768,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
case let .controller(contentParentNode):
var projectedFrame: CGRect = convertFrame(contentParentNode.sourceNode.bounds, from: contentParentNode.sourceNode.view, to: self.view)
var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view)
switch self.source {
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let (sourceNode, sourceRect) = transitionInfo?.sourceNode() {
projectedFrame = convertFrame(sourceRect, from: sourceNode.view, to: self.view)
if let (sourceView, sourceRect) = transitionInfo?.sourceNode() {
projectedFrame = convertFrame(sourceRect, from: sourceView, to: self.view)
}
default:
break
@ -2150,9 +2150,9 @@ public extension ContextExtractedContentSource {
public final class ContextControllerTakeControllerInfo {
public let contentAreaInScreenSpace: CGRect
public let sourceNode: () -> (ASDisplayNode, CGRect)?
public let sourceNode: () -> (UIView, CGRect)?
public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (ASDisplayNode, CGRect)?) {
public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (UIView, CGRect)?) {
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.sourceNode = sourceNode
}

View file

@ -89,7 +89,7 @@ open class AlertController: ViewController, StandalonePresentableController {
private weak var existingAlertController: AlertController?
public var willDismiss: (() -> Void)?
public var dismissed: (() -> Void)?
public var dismissed: ((Bool) -> Void)?
public init(theme: AlertControllerTheme, contentNode: AlertContentNode, existingAlertController: AlertController? = nil, allowInputInset: Bool = true) {
self.theme = theme
@ -108,6 +108,7 @@ open class AlertController: ViewController, StandalonePresentableController {
fatalError("init(coder:) has not been implemented")
}
private var isDismissed = false
override open func loadDisplayNode() {
self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset)
self.displayNodeDidLoad()
@ -118,6 +119,8 @@ open class AlertController: ViewController, StandalonePresentableController {
if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap {
strongSelf.willDismiss?()
strongSelf.controllerNode.animateOut {
self?.dismissed?(true)
self?.isDismissed = true
self?.dismiss()
}
}
@ -140,7 +143,10 @@ open class AlertController: ViewController, StandalonePresentableController {
}
override open func dismiss(completion: (() -> Void)? = nil) {
self.dismissed?()
if !self.isDismissed {
self.isDismissed = true
self.dismissed?(false)
}
self.presentingViewController?.dismiss(animated: false, completion: completion)
}

View file

@ -625,8 +625,8 @@ public extension ContainedViewLayoutTransition {
} else {
switch self {
case .immediate:
view.layer.removeAnimation(forKey: "position")
view.layer.removeAnimation(forKey: "bounds")
//view.layer.removeAnimation(forKey: "position")
//view.layer.removeAnimation(forKey: "bounds")
view.frame = frame
if let completion = completion {
completion(true)

View file

@ -115,12 +115,12 @@ public final class ContextExtractedContentView: UIView {
}
public final class ContextControllerContentNode: ASDisplayNode {
public let sourceNode: ASDisplayNode
public let sourceView: UIView
public let controller: ViewController
private let tapped: () -> Void
public init(sourceNode: ASDisplayNode, controller: ViewController, tapped: @escaping () -> Void) {
self.sourceNode = sourceNode
public init(sourceView: UIView, controller: ViewController, tapped: @escaping () -> Void) {
self.sourceView = sourceView
self.controller = controller
self.tapped = tapped

View file

@ -271,7 +271,7 @@ open class ContextControllerSourceView: UIView {
public weak var additionalActivationProgressLayer: CALayer?
public var targetNodeForActivationProgress: ASDisplayNode?
public var targetViewForActivationProgress: UIView?
public var targetLayerForActivationProgress: CALayer?
public weak var targetLayerForActivationProgress: CALayer?
public var targetNodeForActivationProgressContentRect: CGRect?
public var useSublayerTransformForActivation: Bool = true

View file

@ -1126,6 +1126,10 @@ open class TextNode: ASDisplayNode {
rightOffset = ceil(secondaryRightOffset)
}
if embeddedItems.count > 25 {
assert(true)
}
embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item))
}
@ -1136,6 +1140,9 @@ open class TextNode: ASDisplayNode {
isLastLine = true
}
if isLastLine {
if attributedString.string.hasPrefix("😀") {
assert(true)
}
if first {
first = false
} else {
@ -1175,7 +1182,7 @@ open class TextNode: ASDisplayNode {
for run in runs {
let runAttributes: NSDictionary = CTRunGetAttributes(run)
if let _ = runAttributes["CTForegroundColorFromContext"] {
brokenLineRange.length = CTRunGetStringRange(run).location
brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location
break
}
}

View file

@ -1715,7 +1715,7 @@ private final class PlaybackButtonNode: HighlightTrackingButtonNode {
self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: "15", font: Font.with(size: 11.0, design: .round, weight: .semibold, traits: []), textColor: .white)
super.init(pointerStyle: .circle)
super.init(pointerStyle: nil)
self.addSubnode(self.backgroundIconNode)
self.addSubnode(self.textNode)

View file

@ -764,7 +764,7 @@ func importStickerPackTitleController(context: AccountContext, title: String, te
controller.willDismiss = { [weak contentNode] in
contentNode?.inputFieldNode.deactivateInput()
}
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] animated in
@ -854,7 +854,7 @@ func importStickerPackShortNameController(context: AccountContext, title: String
controller.willDismiss = { [weak contentNode] in
contentNode?.inputFieldNode.deactivateInput()
}
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] animated in

View file

@ -386,6 +386,10 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else if url.path.hasPrefix("/addemoji/") {
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
iconText = NSAttributedString(string: "E", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
@ -432,6 +436,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
title = NSAttributedString(string: tempTitleString as String, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if url.path.hasPrefix("/addstickers/") {
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else if url.path.hasPrefix("/addemoji/") {
iconText = NSAttributedString(string: "E", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
}

View file

@ -1326,7 +1326,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.dismissAllTooltips()
completion()
})])
controller.dismissed = { [weak self] in
controller.dismissed = { [weak self] _ in
self?.isDismissing = false
}
self.present(controller, in: .window(.root))

View file

@ -438,7 +438,7 @@ private func commitChannelOwnershipTransferController(context: AccountContext, u
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
disposable.dispose()
}

View file

@ -375,7 +375,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -796,6 +796,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
guard let product = self.products?.first(where: { $0.id == self.selectedProductId }) else {
return
}
let (currency, amount) = product.priceCurrencyAndAmount
let duration: Int32
switch product.id {
@ -814,81 +815,92 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
self.paymentDisposable.set((inAppPurchaseManager.buyProduct(product, targetPeerId: self.peerId)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|> castError(AssignAppStoreTransactionError.self)
|> take(until: { view in
if let peer = view.peers[view.peerId], peer.isPremium {
return SignalTakeAction(passthrough: false, complete: true)
} else {
return SignalTakeAction(passthrough: false, complete: false)
}
})
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
return .never()
}
|> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout))
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}, completed: { [weak self] in
if let strongSelf = self {
Queue.mainQueue().after(2.0) {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .gift(peerId: self.peerId, currency: currency, amount: amount))
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product, targetPeerId: strongSelf.peerId)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|> castError(AssignAppStoreTransactionError.self)
|> take(until: { view in
if let peer = view.peers[view.peerId], peer.isPremium {
return SignalTakeAction(passthrough: false, complete: true)
} else {
return SignalTakeAction(passthrough: false, complete: false)
}
})
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
return .never()
}
|> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout))
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}, completed: { [weak self] in
if let strongSelf = self {
Queue.mainQueue().after(2.0) {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion(duration)
}
}
}))
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion(duration)
}
}
}))
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
strongSelf.updated(transition: .immediate)
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText = errorText {
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText = errorText {
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}
}))
} else {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
}
}
}))
})
}
func updateIsFocused(_ isFocused: Bool) {

View file

@ -1321,7 +1321,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.updateInProgress(true)
self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium()
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .subscription)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
if available {

View file

@ -409,7 +409,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in

View file

@ -363,7 +363,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
}))
})
])
alertController.dismissed = {
alertController.dismissed = { _ in
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_cancel")
}
presentControllerImpl?(alertController, nil)

View file

@ -17,6 +17,7 @@ import UndoUI
public enum ArchivedStickerPacksControllerMode {
case stickers
case masks
case emoji
}
private final class ArchivedStickerPacksControllerArguments {
@ -237,6 +238,8 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv
switch mode {
case .stickers:
namespace = .stickers
case .emoji:
namespace = .emoji
case .masks:
namespace = .masks
}

View file

@ -26,6 +26,7 @@ private final class InstalledStickerPacksControllerArguments {
let removePack: (ArchivedStickerPackItem) -> Void
let openStickersBot: () -> Void
let openMasks: () -> Void
let openEmoji: () -> Void
let openQuickReaction: () -> Void
let openFeatured: () -> Void
let openArchived: ([ArchivedStickerPackItem]?) -> Void
@ -35,13 +36,14 @@ private final class InstalledStickerPacksControllerArguments {
let expandTrendingPacks: () -> Void
let addPack: (StickerPackCollectionInfo) -> Void
init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) {
init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openEmoji: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) {
self.account = account
self.openStickerPack = openStickerPack
self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions
self.removePack = removePack
self.openStickersBot = openStickersBot
self.openMasks = openMasks
self.openEmoji = openEmoji
self.openQuickReaction = openQuickReaction
self.openFeatured = openFeatured
self.openArchived = openArchived
@ -83,6 +85,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
case trending(PresentationTheme, String, Int32)
case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?)
case masks(PresentationTheme, String)
case emoji(PresentationTheme, String)
case quickReaction(String, UIImage?)
case animatedStickers(PresentationTheme, String, Bool)
case animatedStickersInfo(PresentationTheme, String)
@ -95,7 +98,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo:
case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo:
return InstalledStickerPacksSection.service.rawValue
case .trendingPacksTitle, .trendingPack, .trendingExpand:
return InstalledStickerPacksSection.trending.rawValue
@ -114,24 +117,26 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
return .index(2)
case .masks:
return .index(3)
case .quickReaction:
case .emoji:
return .index(4)
case .animatedStickers:
case .quickReaction:
return .index(5)
case .animatedStickersInfo:
case .animatedStickers:
return .index(6)
case .trendingPacksTitle:
case .animatedStickersInfo:
return .index(7)
case .trendingPacksTitle:
return .index(8)
case let .trendingPack(_, _, _, info, _, _, _, _, _):
return .trendingPack(info.id)
case .trendingExpand:
return .index(8)
case .packsTitle:
return .index(9)
case .packsTitle:
return .index(10)
case let .pack(_, _, _, info, _, _, _, _, _, _):
return .pack(info.id)
case .packsInfo:
return .index(10)
return .index(11)
}
}
@ -155,6 +160,12 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
} else {
return false
}
case let .emoji(lhsTheme, lhsCount):
if case let .emoji(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount {
return true
} else {
return false
}
case let .quickReaction(lhsText, lhsImage):
if case let .quickReaction(rhsText, rhsImage) = rhs, lhsText == rhsText, lhsImage === rhsImage {
return true
@ -305,30 +316,37 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
default:
return true
}
case .emoji:
switch rhs {
case .suggestOptions, .trending, .archived, .masks, .emoji:
return false
default:
return true
}
case .quickReaction:
switch rhs {
case .suggestOptions, .trending, .archived, .masks, .quickReaction:
case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction:
return false
default:
return true
}
case .animatedStickers:
switch rhs {
case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers:
case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction, .animatedStickers:
return false
default:
return true
}
case .animatedStickersInfo:
switch rhs {
case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers, .animatedStickersInfo:
case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction, .animatedStickers, .animatedStickersInfo:
return false
default:
return true
}
case .trendingPacksTitle:
switch rhs {
case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle:
case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle:
return false
default:
return true
@ -344,14 +362,14 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
}
case .trendingExpand:
switch rhs {
case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand:
case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand:
return false
default:
return true
}
case .packsTitle:
switch rhs {
case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle:
case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle:
return false
default:
return true
@ -390,6 +408,10 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openMasks()
})
case let .emoji(_, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openEmoji()
})
case let .quickReaction(title, image):
let labelStyle: ItemListDisclosureLabelStyle
if let image = image {
@ -505,6 +527,8 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It
return Namespaces.ItemCollection.CloudStickerPacks
case .masks:
return Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
@ -544,6 +568,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
}
entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title))
//TODO:localize
entries.append(.emoji(presentationData.theme, "Emoji"))
entries.append(.quickReaction(presentationData.strings.Settings_QuickReactionSetup_NavigationTitle, quickReactionImage))
entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers))
@ -576,6 +603,11 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
if let archived = archived, !archived.isEmpty {
entries.append(.archived(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedMasks, Int32(archived.count), archived))
}
case .emoji:
if let archived = archived, !archived.isEmpty {
//TODO:localize
entries.append(.archived(presentationData.theme, "Archived Emoji", Int32(archived.count), archived))
}
}
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespaceForMode(mode)])] as? ItemCollectionInfosView {
@ -618,6 +650,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
markdownString = presentationData.strings.StickerPacksSettings_ManagingHelp
case .masks:
markdownString = presentationData.strings.MaskStickerSettings_Info
case .emoji:
//TODO:localize
markdownString = "Emoji"
}
let entities = generateTextEntities(markdownString, enabledTypes: [.mention])
if let entity = entities.first {
@ -633,6 +668,7 @@ public enum InstalledStickerPacksControllerMode {
case general
case modal
case masks
case emoji
}
public func installedStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, archivedPacks: [ArchivedStickerPackItem]? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void = { _ in }, focusOnItemTag: InstalledStickerPacksEntryTag? = nil) -> ViewController {
@ -731,6 +767,8 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
}))
}, openMasks: {
pushControllerImpl?(installedStickerPacksController(context: context, mode: .masks, archivedPacks: archivedPacks, updatedPacks: { _ in}))
}, openEmoji: {
pushControllerImpl?(installedStickerPacksController(context: context, mode: .emoji, archivedPacks: archivedPacks, updatedPacks: { _ in}))
}, openQuickReaction: {
pushControllerImpl?(quickReactionSetupController(
context: context
@ -742,6 +780,8 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
switch mode {
case .masks:
archivedMode = .masks
case .emoji:
archivedMode = .emoji
default:
archivedMode = .stickers
}
@ -876,6 +916,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
featured.set(.single([]))
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init)))
quickReactionImage = .single(nil)
case .emoji:
featured.set(.single([]))
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .emoji) |> map(Optional.init)))
quickReactionImage = .single(nil)
}
var previousPackCount: Int?
@ -1027,6 +1071,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
title = presentationData.strings.StickerPacksSettings_Title
case .masks:
title = presentationData.strings.MaskStickerSettings_Title
case .emoji:
//TODO:localize
title = "Emoji"
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)

View file

@ -1259,7 +1259,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -1405,7 +1405,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceNode.bounds)
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}

View file

@ -620,6 +620,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_RemoveEmojiCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count)
}
@ -628,6 +630,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_AddStickerCount(info.count)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_AddEmojiCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_AddMaskCount(info.count)
}

View file

@ -701,6 +701,8 @@ private final class StickerPackContainer: ASDisplayNode {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_RemoveStickerCount(Int32(entries.count))
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_RemoveEmojiCount(Int32(entries.count))
} else {
text = self.presentationData.strings.StickerPack_RemoveMaskCount(Int32(entries.count))
}
@ -710,6 +712,8 @@ private final class StickerPackContainer: ASDisplayNode {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_AddStickerCount(Int32(entries.count))
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_AddEmojiCount(Int32(entries.count))
} else {
text = self.presentationData.strings.StickerPack_AddMaskCount(Int32(entries.count))
}

View file

@ -708,7 +708,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-875679776] = { return Api.StatsPercentValue.parse_statsPercentValue($0) }
dict[1202287072] = { return Api.StatsURL.parse_statsURL($0) }
dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) }
dict[-673242758] = { return Api.StickerSet.parse_stickerSet($0) }
dict[768691932] = { return Api.StickerSet.parse_stickerSet($0) }
dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) }
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
dict[-1609668650] = { return Api.Theme.parse_theme($0) }

View file

@ -1,12 +1,12 @@
public extension Api {
enum StickerSet: TypeConstructorDescription {
case stickerSet(flags: Int32, installedDate: Int32?, id: Int64, accessHash: Int64, title: String, shortName: String, thumbs: [Api.PhotoSize]?, thumbDcId: Int32?, thumbVersion: Int32?, count: Int32, hash: Int32)
case stickerSet(flags: Int32, installedDate: Int32?, id: Int64, accessHash: Int64, title: String, shortName: String, thumbs: [Api.PhotoSize]?, thumbDcId: Int32?, thumbVersion: Int32?, thumbDocumentId: Int64?, count: Int32, hash: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let count, let hash):
case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let thumbDocumentId, let count, let hash):
if boxed {
buffer.appendInt32(-673242758)
buffer.appendInt32(768691932)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(installedDate!, buffer: buffer, boxed: false)}
@ -21,6 +21,7 @@ public extension Api {
}}
if Int(flags) & Int(1 << 4) != 0 {serializeInt32(thumbDcId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeInt32(thumbVersion!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt64(thumbDocumentId!, buffer: buffer, boxed: false)}
serializeInt32(count, buffer: buffer, boxed: false)
serializeInt32(hash, buffer: buffer, boxed: false)
break
@ -29,8 +30,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let count, let hash):
return ("stickerSet", [("flags", String(describing: flags)), ("installedDate", String(describing: installedDate)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("title", String(describing: title)), ("shortName", String(describing: shortName)), ("thumbs", String(describing: thumbs)), ("thumbDcId", String(describing: thumbDcId)), ("thumbVersion", String(describing: thumbVersion)), ("count", String(describing: count)), ("hash", String(describing: hash))])
case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let thumbDocumentId, let count, let hash):
return ("stickerSet", [("flags", String(describing: flags)), ("installedDate", String(describing: installedDate)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("title", String(describing: title)), ("shortName", String(describing: shortName)), ("thumbs", String(describing: thumbs)), ("thumbDcId", String(describing: thumbDcId)), ("thumbVersion", String(describing: thumbVersion)), ("thumbDocumentId", String(describing: thumbDocumentId)), ("count", String(describing: count)), ("hash", String(describing: hash))])
}
}
@ -55,10 +56,12 @@ public extension Api {
if Int(_1!) & Int(1 << 4) != 0 {_8 = reader.readInt32() }
var _9: Int32?
if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() }
var _10: Int32?
_10 = reader.readInt32()
var _10: Int64?
if Int(_1!) & Int(1 << 8) != 0 {_10 = reader.readInt64() }
var _11: Int32?
_11 = reader.readInt32()
var _12: Int32?
_12 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = _3 != nil
@ -68,10 +71,11 @@ public extension Api {
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil
let _c10 = _10 != nil
let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil
let _c11 = _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
return Api.StickerSet.stickerSet(flags: _1!, installedDate: _2, id: _3!, accessHash: _4!, title: _5!, shortName: _6!, thumbs: _7, thumbDcId: _8, thumbVersion: _9, count: _10!, hash: _11!)
let _c12 = _12 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
return Api.StickerSet.stickerSet(flags: _1!, installedDate: _2, id: _3!, accessHash: _4!, title: _5!, shortName: _6!, thumbs: _7, thumbDcId: _8, thumbVersion: _9, thumbDocumentId: _10, count: _11!, hash: _12!)
}
else {
return nil

View file

@ -6295,11 +6295,11 @@ public extension Api.functions.payments {
}
}
public extension Api.functions.payments {
static func canPurchasePremium() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1435856696)
return (FunctionDescription(name: "payments.canPurchasePremium", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
buffer.appendInt32(-1614700874)
purpose.serialize(buffer, true)
return (FunctionDescription(name: "payments.canPurchasePremium", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {

View file

@ -805,7 +805,7 @@ public final class MediaStreamComponent: CombinedComponent {
"Point 3.Group 1.Fill 1": whiteColor,
"Point 1.Group 1.Fill 1": whiteColor
],
mode: .still
mode: .still(position: .begin)
),
size: CGSize(width: 22.0, height: 22.0)
).tagged(moreAnimationTag))),

View file

@ -460,7 +460,7 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account:
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] animated in
@ -758,7 +758,7 @@ func voiceChatUserNameController(sharedContext: SharedAccountContext, account: A
contentNode?.firstNameInputFieldNode.updateTheme(presentationData.theme)
contentNode?.lastNameInputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] animated in

View file

@ -1078,6 +1078,7 @@ public class Account {
self.managedOperationsDisposable.add(managedSynchronizeGroupedPeersOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .stickers).start())
self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .masks).start())
self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .emoji).start())
self.managedOperationsDisposable.add(managedSynchronizeMarkFeaturedStickerPacksAsSeenOperations(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedSynchronizeRecentlyUsedMediaOperations(postbox: self.postbox, network: self.network, category: .stickers, revalidationContext: self.mediaReferenceRevalidationContext).start())
self.managedOperationsDisposable.add(managedSynchronizeSavedGifsOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start())

View file

@ -1,7 +1,7 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
extension MediaResourceReference {
var apiFileReference: Data? {
@ -227,6 +227,7 @@ private enum MediaReferenceRevalidationKey: Hashable {
case peerAvatars(peer: PeerReference)
case attachBot(peer: PeerReference)
case notificationSoundList
case customEmoji(fileId: Int64)
}
private final class MediaReferenceRevalidationItemContext {
@ -391,6 +392,26 @@ final class MediaReferenceRevalidationContext {
}
}
func customEmoji(postbox: Postbox, network: Network, background: Bool, fileId: Int64) -> Signal<TelegramMediaFile, RevalidateMediaReferenceError> {
return network.request(Api.functions.messages.getCustomEmojiDocuments(documentId: [fileId]))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.Document]?, NoError> in
return .single(nil)
}
|> castError(RevalidateMediaReferenceError.self)
|> mapToSignal { result -> Signal<TelegramMediaFile, RevalidateMediaReferenceError> in
guard let result = result else {
return .fail(.generic)
}
for document in result {
if let file = telegramMediaFileFromApiDocument(document) {
return .single(file)
}
}
return .fail(.generic)
}
}
func webPage(postbox: Postbox, network: Network, background: Bool, webPage: WebpageReference) -> Signal<TelegramMediaWebpage, RevalidateMediaReferenceError> {
return self.genericItem(key: .webPage(webPage: webPage), background: background, request: { next, error in
return (updatedRemoteWebpage(postbox: postbox, network: network, webPage: webPage)
@ -726,7 +747,37 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali
}
}
return .fail(.generic)
}
case let .customEmoji(media):
if let file = media as? TelegramMediaFile {
return revalidationContext.customEmoji(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, fileId: file.fileId.id)
|> mapToSignal { result -> Signal<RevalidatedMediaResource, RevalidateMediaReferenceError> in
if let updatedResource = findUpdatedMediaResource(media: result, previousMedia: media, resource: resource) {
return postbox.transaction { transaction -> RevalidatedMediaResource in
if let id = media.id {
var attributes = result.attributes
if !attributes.contains(where: { attribute in
if case .hintIsValidated = attribute {
return true
} else {
return false
}
}) {
attributes.append(.hintIsValidated)
}
let file = result.withUpdatedAttributes(attributes)
updateMessageMedia(transaction: transaction, id: id, media: file)
}
return RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)
}
|> castError(RevalidateMediaReferenceError.self)
} else {
return .fail(.generic)
}
}
} else {
return .fail(.generic)
}
}
case let .avatar(peer, _):
return revalidationContext.peer(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer)
|> mapToSignal { updatedPeer -> Signal<RevalidatedMediaResource, RevalidateMediaReferenceError> in

View file

@ -1379,6 +1379,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
let namespace: SynchronizeInstalledStickerPacksOperationNamespace
if (flags & (1 << 0)) != 0 {
namespace = .masks
} else if (flags & (1 << 1)) != 0 {
namespace = .emoji
} else {
namespace = .stickers
}
@ -3459,9 +3461,11 @@ func replayFinalState(
}) {
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false)
} else {
var syncStickers = false
var syncMasks = false
var syncEmoji = false
loop: for operation in stickerPackOperations {
switch operation {
case let .add(apiSet):
@ -3501,6 +3505,8 @@ func replayFinalState(
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
@ -3512,6 +3518,8 @@ func replayFinalState(
continue loop
} else if namespace == Namespaces.ItemCollection.CloudStickerPacks && syncStickers {
continue loop
} else if namespace == Namespaces.ItemCollection.CloudEmojiPacks && syncEmoji {
continue loop
}
var updatedInfos = transaction.getItemCollectionsInfos(namespace: info.id.namespace).map { $0.1 as! StickerPackCollectionInfo }
@ -3532,6 +3540,8 @@ func replayFinalState(
collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
if Set(currentInfos.map { $0.id.id }) != Set(ids) {
@ -3540,6 +3550,8 @@ func replayFinalState(
syncStickers = true
case .masks:
syncMasks = true
case .emoji:
syncEmoji = true
}
} else {
var currentDict: [ItemCollectionId: StickerPackCollectionInfo] = [:]
@ -3556,6 +3568,7 @@ func replayFinalState(
case .sync:
syncStickers = true
syncMasks = true
syncEmoji = true
break loop
}
}
@ -3565,6 +3578,9 @@ func replayFinalState(
if syncMasks {
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false)
}
if syncEmoji {
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false)
}
}
}

View file

@ -72,6 +72,8 @@ func managedSynchronizeInstalledStickerPacksOperations(postbox: Postbox, network
tag = OperationLogTags.SynchronizeInstalledStickerPacks
case .masks:
tag = OperationLogTags.SynchronizeInstalledMasks
case .emoji:
tag = OperationLogTags.SynchronizeInstalledEmoji
}
let helper = Atomic<ManagedSynchronizeInstalledStickerPacksOperationsHelper>(value: ManagedSynchronizeInstalledStickerPacksOperationsHelper())
@ -126,7 +128,7 @@ private func hashForStickerPackInfos(_ infos: [StickerPackCollectionInfo]) -> In
var acc: UInt64 = 0
for info in infos {
combineInt64Hash(&acc, with: UInt64(UInt32(bitPattern: info.hash)))
combineInt64Hash(&acc, with: UInt64(bitPattern: Int64(info.hash)))
}
return finalizeInt64Hash(acc)
@ -295,6 +297,8 @@ private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeI
break
case .masks:
flags |= (1 << 0)
case .emoji:
flags |= (1 << 1)
}
return network.request(Api.functions.messages.reorderStickerSets(flags: flags, order: ids.map { $0.id }))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
@ -312,6 +316,8 @@ private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox:
collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
@ -435,6 +441,8 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction,
collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
@ -446,6 +454,8 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction,
request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash))
case .masks:
request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash))
case .emoji:
request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash))
}
let sequence = request

View file

@ -17,6 +17,7 @@ func manageStickerPacks(network: Network, postbox: Postbox) -> Signal<Void, NoEr
return (postbox.transaction { transaction -> Void in
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false)
addSynchronizeSavedGifsOperation(transaction: transaction, operation: .sync)
addSynchronizeSavedStickersOperation(transaction: transaction, operation: .sync)
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .sync)

View file

@ -14,6 +14,8 @@ public func addSynchronizeInstalledStickerPacksOperation(transaction: Transactio
operationNamespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
operationNamespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
operationNamespace = .emoji
default:
return
}
@ -31,6 +33,9 @@ func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, name
case .masks:
tag = OperationLogTags.SynchronizeInstalledMasks
itemCollectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
tag = OperationLogTags.SynchronizeInstalledEmoji
itemCollectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
var previousStickerPackIds: [ItemCollectionId]?
var archivedPacks: [ItemCollectionId] = []

View file

@ -225,6 +225,7 @@ public enum AnyMediaReference: Equatable {
case savedGif(media: Media)
case avatarList(peer: PeerReference, media: Media)
case attachBot(peer: PeerReference, media: Media)
case customEmoji(media: Media)
public static func ==(lhs: AnyMediaReference, rhs: AnyMediaReference) -> Bool {
switch lhs {
@ -270,6 +271,12 @@ public enum AnyMediaReference: Equatable {
} else {
return false
}
case let .customEmoji(lhsMedia):
if case let .customEmoji(rhsMedia) = rhs, lhsMedia.isEqual(to: rhsMedia) {
return true
} else {
return false
}
}
}
@ -289,6 +296,8 @@ public enum AnyMediaReference: Equatable {
return nil
case .attachBot:
return nil
case .customEmoji:
return nil
}
}
@ -322,6 +331,10 @@ public enum AnyMediaReference: Equatable {
if let media = media as? T {
return .attachBot(peer: peer, media: media)
}
case let .customEmoji(media):
if let media = media as? T {
return .customEmoji(media: media)
}
}
return nil
}
@ -342,6 +355,8 @@ public enum AnyMediaReference: Equatable {
return media
case let .attachBot(_, media):
return media
case let .customEmoji(media):
return media
}
}
@ -421,6 +436,7 @@ public enum MediaReference<T: Media> {
case savedGif
case avatarList
case attachBot
case customEmoji
}
case standalone(media: T)
@ -430,6 +446,7 @@ public enum MediaReference<T: Media> {
case savedGif(media: T)
case avatarList(peer: PeerReference, media: T)
case attachBot(peer: PeerReference, media: T)
case customEmoji(media: T)
public init?(decoder: PostboxDecoder) {
guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else {
@ -476,6 +493,11 @@ public enum MediaReference<T: Media> {
return nil
}
self = .attachBot(peer: peer, media: media)
case .customEmoji:
guard let media = decoder.decodeObjectForKey("m") as? T else {
return nil
}
self = .customEmoji(media: media)
}
}
@ -507,6 +529,9 @@ public enum MediaReference<T: Media> {
encoder.encodeInt32(CodingCase.attachBot.rawValue, forKey: "_r")
encoder.encodeObject(peer, forKey: "pr")
encoder.encodeObject(media, forKey: "m")
case let .customEmoji(media):
encoder.encodeInt32(CodingCase.customEmoji.rawValue, forKey: "_r")
encoder.encodeObject(media, forKey: "m")
}
}
@ -526,6 +551,8 @@ public enum MediaReference<T: Media> {
return .avatarList(peer: peer, media: media)
case let .attachBot(peer, media):
return .attachBot(peer: peer, media: media)
case let .customEmoji(media):
return .customEmoji(media: media)
}
}
@ -549,6 +576,8 @@ public enum MediaReference<T: Media> {
return media
case let .attachBot(_, media):
return media
case let .customEmoji(media):
return media
}
}

View file

@ -47,6 +47,7 @@ public struct Namespaces {
public static let CloudAnimatedEmojiAnimations: Int32 = 5
public static let CloudAnimatedEmojiReactions: Int32 = 6
public static let CloudPremiumGifts: Int32 = 7
public static let CloudEmojiPacks: Int32 = 8
}
public struct OrderedItemList {
@ -165,6 +166,7 @@ public struct OperationLogTags {
public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19)
public static let SynchronizeChatListFilters = PeerOperationLogTag(value: 20)
public static let SynchronizeMarkAllUnseenReactions = PeerOperationLogTag(value: 21)
public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22)
}
public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {

View file

@ -27,6 +27,9 @@ public struct StickerPackCollectionInfoFlags: OptionSet {
if flags.contains(StickerPackCollectionInfoFlags.isVideo) {
rawValue |= StickerPackCollectionInfoFlags.isVideo.rawValue
}
if flags.contains(StickerPackCollectionInfoFlags.isEmoji) {
rawValue |= StickerPackCollectionInfoFlags.isEmoji.rawValue
}
self.rawValue = rawValue
}
@ -35,6 +38,7 @@ public struct StickerPackCollectionInfoFlags: OptionSet {
public static let isOfficial = StickerPackCollectionInfoFlags(rawValue: 1 << 1)
public static let isAnimated = StickerPackCollectionInfoFlags(rawValue: 1 << 2)
public static let isVideo = StickerPackCollectionInfoFlags(rawValue: 1 << 3)
public static let isEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 4)
}

View file

@ -4,6 +4,7 @@ import Postbox
public enum SynchronizeInstalledStickerPacksOperationNamespace: Int32 {
case stickers = 0
case masks = 1
case emoji
}
public final class SynchronizeInstalledStickerPacksOperation: PostboxCoding {

View file

@ -605,6 +605,40 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
return false
}
public var isCustomEmoji: Bool {
var hasSticker = false
for attribute in self.attributes {
if case .CustomEmoji = attribute {
hasSticker = true
break
}
}
return hasSticker
}
public var isPremiumEmoji: Bool {
for attribute in self.attributes {
if case let .CustomEmoji(isPremium, _, _) = attribute {
return isPremium
}
}
return false
}
public var isVideoEmoji: Bool {
if self.mimeType == "video/webm" {
var hasSticker = false
for attribute in self.attributes {
if case .CustomEmoji = attribute {
hasSticker = true
break
}
}
return hasSticker
}
return false
}
public var hasLinkedStickers: Bool {
for attribute in self.attributes {
if case .HasLinkedStickers = attribute {

View file

@ -58,7 +58,7 @@ public enum RestoreAppStoreReceiptError {
case generic
}
func _internal_canPurchasePremium(account: Account) -> Signal<Bool, NoError> {
func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
return account.network.request(Api.functions.payments.canPurchasePremium())
|> map { result -> Bool in
switch result {

View file

@ -42,8 +42,8 @@ public extension TelegramEngine {
return _internal_sendAppStoreReceipt(account: self.account, receipt: receipt, purpose: purpose)
}
public func canPurchasePremium() -> Signal<Bool, NoError> {
return _internal_canPurchasePremium(account: self.account)
public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
return _internal_canPurchasePremium(account: self.account, purpose: purpose)
}
}
}

View file

@ -547,6 +547,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}

View file

@ -7,6 +7,7 @@ import SwiftSignalKit
public enum ArchivedStickerPacksNamespace: Int32 {
case stickers = 0
case masks = 1
case emoji = 2
var itemCollectionNamespace: ItemCollectionId.Namespace {
switch self {
@ -14,6 +15,8 @@ public enum ArchivedStickerPacksNamespace: Int32 {
return Namespaces.ItemCollection.CloudStickerPacks
case .masks:
return Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
}
@ -32,6 +35,8 @@ func _internal_archivedStickerPacks(account: Account, namespace: ArchivedSticker
var flags: Int32 = 0
if case .masks = namespace {
flags |= 1 << 0
} else if case .emoji = namespace {
flags |= 1 << 1
}
return account.network.request(Api.functions.messages.getArchivedStickers(flags: flags, offsetId: 0, limit: 200))
|> map { result -> [ArchivedStickerPackItem] in

View file

@ -168,7 +168,7 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference:
}
func cachedStickerPack(transaction: Transaction, reference: StickerPackReference) -> (StickerPackCollectionInfo, [StickerPackItem], Bool)? {
let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks]
let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks, Namespaces.ItemCollection.CloudEmojiPacks]
switch reference {
case let .id(id, _):
for namespace in namespaces {

View file

@ -173,6 +173,8 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}

View file

@ -54,6 +54,8 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}

View file

@ -46,6 +46,9 @@ extension StickerPackCollectionInfo {
if (flags & (1 << 6)) != 0 {
setFlags.insert(.isVideo)
}
if (flags & (1 << 7)) != 0 {
setFlags.insert(.isEmoji)
}
var thumbnailRepresentation: TelegramMediaImageRepresentation?
var immediateThumbnailData: Data?

View file

@ -11,6 +11,8 @@ func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCo
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
namespace = .emoji
default:
namespace = nil
}
@ -57,6 +59,8 @@ func _internal_removeStickerPacksInteractively(postbox: Postbox, ids: [ItemColle
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
namespace = .emoji
default:
namespace = nil
}

View file

@ -276,6 +276,7 @@ public enum PresentationResourceKey: Int32 {
case chatKeyboardActionButtonProfileIcon
case chatKeyboardActionButtonAddToChatIcon
case chatKeyboardActionButtonWebAppIcon
case chatEntityKeyboardLock
case uploadToneIcon
}

View file

@ -1281,4 +1281,10 @@ public struct PresentationResourcesChat {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotWebApp"), color: theme.chat.inputButtonPanel.buttonTextColor)
})
}
public static func chatEntityKeyboardLock(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatEntityKeyboardLock.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.chat.inputMediaPanel.stickersSectionTextColor)
})
}
}

View file

@ -69,7 +69,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
super.init()
if let file = file {
self.updateFile(file: file, attemptSynchronousLoad: true)
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
} else {
self.infoDisposable = (context.engine.stickers.resolveInlineSticker(fileId: emoji.fileId)
|> deliverOnMainQueue).start(next: { [weak self] file in
@ -130,10 +130,30 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.loadAnimation()
} else {
self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] _ in
self?.loadAnimation()
let pointSize = self.pointSize
let placeholderColor = self.placeholderColor
self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in
if !result {
MultiAnimationRendererImpl.firstFrameQueue.async {
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
if let image = image {
strongSelf.contents = image.cgImage
}
strongSelf.loadAnimation()
}
}
} else {
guard let strongSelf = self else {
return
}
strongSelf.loadAnimation()
}
})
self.loadAnimation()
}
}
@ -143,32 +163,51 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
let context = self.context
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
guard let result = result else {
return
}
if file.isAnimatedSticker || file.isVideoEmoji {
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
if file.isVideoSticker {
cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer)
} else {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
writer.finish()
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
guard let result = result else {
return
}
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
if file.isVideoEmoji {
cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer)
} else {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
writer.finish()
return
}
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
}
})
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
} else {
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else {
return
}
cacheStillSticker(path: result.path, width: Int(size.width), height: Int(size.height), writer: writer)
})
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
}
}
}

View file

@ -39,6 +39,8 @@ swift_library(
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/UndoUI:UndoUI",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
],
visibility = [
"//visibility:public",

View file

@ -8,6 +8,8 @@ import TelegramCore
import Postbox
import BlurredBackgroundComponent
import BundleIconComponent
import AudioToolbox
import SwiftSignalKit
public final class EntityKeyboardChildEnvironment: Equatable {
public let theme: PresentationTheme
@ -41,6 +43,11 @@ public final class EntityKeyboardComponent: Component {
}
}
private enum ReorderCategory {
case stickers
case emoji
}
public struct GifSearchEmoji: Equatable {
public var emoji: String
public var file: TelegramMediaFile
@ -199,6 +206,7 @@ public final class EntityKeyboardComponent: Component {
//TODO:localize
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "recent",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/RecentTabIcon",
theme: component.theme,
@ -210,6 +218,7 @@ public final class EntityKeyboardComponent: Component {
))
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "trending",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/TrendingGifs",
theme: component.theme,
@ -222,6 +231,7 @@ public final class EntityKeyboardComponent: Component {
for emoji in component.availableGifSearchEmojies {
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: emoji.emoji,
isReorderable: false,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.stickerContent.context,
file: emoji.file,
@ -248,7 +258,9 @@ public final class EntityKeyboardComponent: Component {
theme: component.theme,
items: topGifItems,
defaultActiveItemId: defaultActiveGifItemId,
activeContentItemIdUpdated: gifsContentItemIdUpdated
activeContentItemIdUpdated: gifsContentItemIdUpdated,
reorderItems: { _ in
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputGifsIcon",
@ -265,56 +277,53 @@ public final class EntityKeyboardComponent: Component {
self?.openSearch()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
/*contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
}
).minSize(CGSize(width: 38.0, height: 38.0)))))*/
var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in component.stickerContent.itemGroups {
if let id = itemGroup.id.base as? String {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: String] = [
"saved": "Chat/Input/Media/SavedStickersTabIcon",
"recent": "Chat/Input/Media/RecentTabIcon",
"premium": "Chat/Input/Media/PremiumIcon"
]
let titleMapping: [String: String] = [
"saved": "Saved",
"recent": "Recent",
"premium": "Premium"
]
if let iconName = iconMapping[id], let title = titleMapping[id] {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.id,
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: iconName,
theme: component.theme,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id)
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
} else {
if !itemGroup.items.isEmpty {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.id,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.stickerContent.context,
file: itemGroup.items[0].file,
animationCache: component.stickerContent.animationCache,
animationRenderer: component.stickerContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id)
}
if let file = itemGroup.items[0].file {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: true,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.stickerContent.context,
file: file,
animationCache: component.stickerContent.animationCache,
animationRenderer: component.stickerContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
))
}
}
}
}
@ -323,7 +332,13 @@ public final class EntityKeyboardComponent: Component {
contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topStickerItems,
activeContentItemIdUpdated: stickersContentItemIdUpdated
activeContentItemIdUpdated: stickersContentItemIdUpdated,
reorderItems: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.reorderPacks(category: .stickers, items: items)
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputStickersIcon",
@ -356,26 +371,53 @@ public final class EntityKeyboardComponent: Component {
var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in component.emojiContent.itemGroups {
if !itemGroup.items.isEmpty {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.id,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context,
file: itemGroup.items[0].file,
animationCache: component.emojiContent.animationCache,
animationRenderer: component.emojiContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.id)
}
))
))
if let id = itemGroup.groupId.base as? String {
if id == "static" {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardStaticStickersPanelComponent(
theme: component.theme,
pressed: { [weak self] subgroupId in
guard let strongSelf = self else {
return
}
strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue)
}
))
))
}
} else {
if let file = itemGroup.items[0].file {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: true,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context,
file: file,
animationCache: component.emojiContent.animationCache,
animationRenderer: component.emojiContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
}
}
}
contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topEmojiItems,
activeContentItemIdUpdated: emojiContentItemIdUpdated
activeContentItemIdUpdated: emojiContentItemIdUpdated,
reorderItems: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.reorderPacks(category: .emoji, items: items)
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputEmojiIcon",
@ -404,9 +446,11 @@ public final class EntityKeyboardComponent: Component {
)),
action: {
deleteBackwards()
AudioServicesPlaySystemSound(1155)
}
).withHoldAction({
deleteBackwards()
AudioServicesPlaySystemSound(1155)
}).minSize(CGSize(width: 38.0, height: 38.0)))))
let panelHideBehavior: PagerComponentPanelHideBehavior
@ -441,6 +485,7 @@ public final class EntityKeyboardComponent: Component {
bottomInset: component.bottomInset,
deleteBackwards: { [weak self] in
self?.component?.emojiContent.inputInteraction.deleteBackwards()
AudioServicesPlaySystemSound(0x451)
}
)),
panelStateUpdated: { [weak self] panelState, transition in
@ -599,11 +644,39 @@ public final class EntityKeyboardComponent: Component {
component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
private func scrollToItemGroup(contentId: String, groupId: AnyHashable) {
private func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?) {
if let pagerView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View {
pagerView.scrollToItemGroup(groupId: groupId)
pagerView.scrollToItemGroup(id: groupId, subgroupId: subgroupId)
}
}
private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) {
guard let component = self.component else {
return
}
var currentIds: [ItemCollectionId] = []
for item in items {
guard let id = item.id.base as? ItemCollectionId else {
continue
}
currentIds.append(id)
}
let namespace: ItemCollectionId.Namespace
switch category {
case .stickers:
namespace = Namespaces.ItemCollection.CloudStickerPacks
case .emoji:
namespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let _ = (component.stickerContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
})
}
}
public func makeView() -> View {

View file

@ -67,8 +67,6 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
private var visibilityFraction: CGFloat = 1.0
private var hasExpandedPanels: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
@ -84,6 +82,8 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
let intrinsicHeight: CGFloat = 41.0
let height = intrinsicHeight
let isExpanded = availableSize.height > 41.0
let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value
var transitionOffsetFraction: CGFloat = 0.0
@ -133,6 +133,10 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
self.addSubview(panelView.view)
}
if !isExpanded {
panelView.isExpanded = false
}
let panelId = panel.id
let _ = panelView.view.update(
transition: panelTransition,
@ -198,6 +202,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
guard let panelView = self.panelViews[id] else {
return
}
if panelView.isExpanded == isExpanded {
return
}
panelView.isExpanded = isExpanded
var hasExpanded = false
@ -207,12 +214,8 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
break
}
}
if self.hasExpandedPanels != hasExpanded {
self.hasExpandedPanels = hasExpanded
self.panelEnvironment?.isExpandedUpdated(self.hasExpandedPanels, transition)
}
self.panelEnvironment?.isExpandedUpdated(hasExpanded, transition)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

View file

@ -11,6 +11,7 @@ import AnimationCache
import MultiAnimationRenderer
import AccountContext
import MultilineTextComponent
import LottieAnimationComponent
final class EntityKeyboardAnimationTopPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
@ -94,14 +95,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
if self.itemLayer == nil {
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
item: EmojiPagerContentComponent.Item(
emoji: "",
file: component.file,
stickerPackItem: nil
staticEmoji: nil,
subgroupId: nil
),
context: component.context,
groupId: "topPanel",
attemptSynchronousLoad: false,
file: component.file,
staticEmoji: nil,
cache: component.animationCache,
renderer: component.animationRenderer,
placeholderColor: .lightGray,
@ -323,30 +325,429 @@ final class EntityKeyboardIconTopPanelComponent: Component {
}
}
final class EntityKeyboardStaticStickersPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
let theme: PresentationTheme
let pressed: (EmojiPagerContentComponent.StaticEmojiSegment) -> Void
init(
theme: PresentationTheme,
pressed: @escaping (EmojiPagerContentComponent.StaticEmojiSegment) -> Void
) {
self.theme = theme
self.pressed = pressed
}
static func ==(lhs: EntityKeyboardStaticStickersPanelComponent, rhs: EntityKeyboardStaticStickersPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var visibleItemViews: [EmojiPagerContentComponent.StaticEmojiSegment: ComponentView<Empty>] = [:]
private var component: EntityKeyboardStaticStickersPanelComponent?
private var ignoreScrolling: Bool = false
override init(frame: CGRect) {
self.scrollView = UIScrollView()
super.init(frame: frame)
self.scrollView.layer.anchorPoint = CGPoint()
self.scrollView.delaysContentTouches = false
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.clipsToBounds = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let scrollViewLocation = recognizer.location(in: self.scrollView)
for (id, itemView) in self.visibleItemViews {
if let view = itemView.view, view.frame.insetBy(dx: -4.0, dy: -4.0).contains(scrollViewLocation) {
self.component?.pressed(id)
break
}
}
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling {
return
}
self.updateVisibleItems(transition: .immediate, animateAppearingItems: true)
}
private func updateVisibleItems(transition: Transition, animateAppearingItems: Bool) {
guard let component = self.component else {
return
}
let _ = component
var validItemIds = Set<EmojiPagerContentComponent.StaticEmojiSegment>()
let visibleBounds = self.scrollView.bounds
let componentHeight: CGFloat = 32.0
let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases
let itemSize: CGFloat = 28.0
let itemSpacing: CGFloat = 4.0
let sideInset: CGFloat = 2.0
for i in 0 ..< items.count {
let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: floor(componentHeight - itemSize) / 2.0), size: CGSize(width: itemSize, height: itemSize))
if visibleBounds.intersects(itemFrame) {
let item = items[i]
validItemIds.insert(item)
let itemView: ComponentView<Empty>
if let current = self.visibleItemViews[item] {
itemView = current
} else {
itemView = ComponentView<Empty>()
self.visibleItemViews[item] = itemView
}
let animationName: String
switch item {
case .people:
animationName = "emojicat_smiles"
case .animalsAndNature:
animationName = "emojicat_animals"
case .foodAndDrink:
animationName = "emojicat_food"
case .activityAndSport:
animationName = "emojicat_activity"
case .travelAndPlaces:
animationName = "emojicat_places"
case .objects:
animationName = "emojicat_objects"
case .symbols:
animationName = "emojicat_symbols"
case .flags:
animationName = "emojicat_flags"
}
let _ = itemView.update(
transition: .immediate,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: animationName,
colors: ["__allcolors__": component.theme.chat.inputMediaPanel.panelIconColor],
mode: animateAppearingItems ? .animating(loop: false) : .still(position: .end)
),
size: CGSize(width: itemSize, height: itemSize)
)),
environment: {},
containerSize: CGSize(width: itemSize, height: itemSize)
)
if let view = itemView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
view.frame = itemFrame
}
}
}
var removedItemIds: [EmojiPagerContentComponent.StaticEmojiSegment] = []
for (id, itemView) in self.visibleItemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
itemView.view?.removeFromSuperview()
}
}
for id in removedItemIds {
self.visibleItemViews.removeValue(forKey: id)
}
}
func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.layer.cornerRadius = availableSize.height / 2.0
let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value
self.component = component
let itemSize: CGFloat = 28.0
let itemSpacing: CGFloat = 4.0
let sideInset: CGFloat = 2.0
let itemCount = EmojiPagerContentComponent.StaticEmojiSegment.allCases.count
self.ignoreScrolling = true
self.scrollView.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(availableSize.width, 150.0), height: availableSize.height))
self.scrollView.contentSize = CGSize(width: sideInset * 2.0 + itemSize * CGFloat(itemCount) + itemSpacing * CGFloat(itemCount - 1), height: availableSize.height)
self.ignoreScrolling = false
self.updateVisibleItems(transition: .immediate, animateAppearingItems: false)
if !itemEnvironment.isHighlighted && self.scrollView.contentOffset.x != 0.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class EntityKeyboardTopPanelItemEnvironment: Equatable {
let isExpanded: Bool
let isHighlighted: Bool
init(isExpanded: Bool) {
init(isExpanded: Bool, isHighlighted: Bool) {
self.isExpanded = isExpanded
self.isHighlighted = isHighlighted
}
static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool {
if lhs.isExpanded != rhs.isExpanded {
return false
}
if lhs.isHighlighted != rhs.isHighlighted {
return false
}
return true
}
}
private final class ReorderGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>?)
private let willBegin: (CGPoint) -> Void
private let began: (ComponentHostView<EntityKeyboardTopPanelItemEnvironment>) -> Void
private let ended: () -> Void
private let moved: (CGFloat) -> Void
private let isActiveUpdated: (Bool) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>?
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (ComponentHostView<EntityKeyboardTopPanelItemEnvironment>) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
self.shouldBegin = shouldBegin
self.willBegin = willBegin
self.began = began
self.ended = ended
self.moved = moved
self.isActiveUpdated = isActiveUpdated
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemView = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
longPressTimer.start()
}
private func stopLongPressTimer() {
self.itemView = nil
self.longPressTimer?.invalidate()
self.longPressTimer = nil
}
override public func reset() {
super.reset()
self.itemView = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
return
}
self.isActiveUpdated(true)
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemView = self.itemView {
self.began(itemView)
}
self.isActiveUpdated(true)
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.numberOfTouches > 1 {
self.isActiveUpdated(false)
self.state = .failed
self.ended()
return
}
if self.state == .possible {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
if allowed {
self.isActiveUpdated(true)
self.itemView = itemView
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
if let itemView = self.itemView {
self.began(itemView)
}
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.isActiveUpdated(false)
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.isActiveUpdated(false)
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
let offset = location.x - initialLocation.x
self.moved(offset)
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
let touchLocation = touch.location(in: self.view)
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
self.state = .failed
}
}
}
}
final class EntityKeyboardTopPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment
final class Item: Equatable {
let id: AnyHashable
let isReorderable: Bool
let content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>
init(id: AnyHashable, content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>) {
init(id: AnyHashable, isReorderable: Bool, content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>) {
self.id = id
self.isReorderable = isReorderable
self.content = content
}
@ -354,6 +755,9 @@ final class EntityKeyboardTopPanelComponent: Component {
if lhs.id != rhs.id {
return false
}
if lhs.isReorderable != rhs.isReorderable {
return false
}
if lhs.content != rhs.content {
return false
}
@ -366,17 +770,20 @@ final class EntityKeyboardTopPanelComponent: Component {
let items: [Item]
let defaultActiveItemId: AnyHashable?
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
let reorderItems: ([Item]) -> Void
init(
theme: PresentationTheme,
items: [Item],
defaultActiveItemId: AnyHashable? = nil,
activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>,
reorderItems: @escaping ([Item]) -> Void
) {
self.theme = theme
self.items = items
self.defaultActiveItemId = defaultActiveItemId
self.activeContentItemIdUpdated = activeContentItemIdUpdated
self.reorderItems = reorderItems
}
static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool {
@ -398,24 +805,79 @@ final class EntityKeyboardTopPanelComponent: Component {
final class View: UIView, UIScrollViewDelegate {
private struct ItemLayout {
struct ItemDescription {
var isStatic: Bool
var isStaticExpanded: Bool
}
struct Item {
var frame: CGRect
var innerFrame: CGRect
}
let sideInset: CGFloat = 7.0
let itemSize: CGSize
let staticItemSize: CGSize
let staticExpandedItemSize: CGSize
let innerItemSize: CGSize
let itemSpacing: CGFloat = 15.0
let itemCount: Int
let contentSize: CGSize
let isExpanded: Bool
let items: [Item]
init(itemCount: Int, isExpanded: Bool, height: CGFloat) {
init(isExpanded: Bool, height: CGFloat, items: [ItemDescription]) {
self.isExpanded = isExpanded
self.itemSize = self.isExpanded ? CGSize(width: 54.0, height: 68.0) : CGSize(width: 32.0, height: 32.0)
self.staticItemSize = self.itemSize
self.staticExpandedItemSize = self.isExpanded ? CGSize(width: 150.0, height: 68.0) : CGSize(width: 150.0, height: 32.0)
self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0)
self.itemCount = itemCount
self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize.width + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: height)
var contentSize = CGSize(width: sideInset, height: height)
var resultItems: [Item] = []
var isFirst = true
let itemY = floor((contentSize.height - self.itemSize.height) / 2.0)
for item in items {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
let currentItemSize: CGSize
if item.isStaticExpanded {
currentItemSize = self.staticExpandedItemSize
} else if item.isStatic {
currentItemSize = self.staticItemSize
} else {
currentItemSize = self.itemSize
}
let frame = CGRect(origin: CGPoint(x: contentSize.width, y: itemY), size: currentItemSize)
var innerFrame = frame
if item.isStaticExpanded {
} else if item.isStatic {
} else {
innerFrame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0
innerFrame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0
innerFrame.size = self.innerItemSize
}
resultItems.append(Item(
frame: frame,
innerFrame: innerFrame
))
contentSize.width += frame.width
}
contentSize.width += sideInset
self.contentSize = contentSize
self.items = resultItems
}
func containerFrame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize.height) / 2.0)), size: self.itemSize)
return self.items[index].frame
}
func contentFrame(containerFrame: CGRect) -> CGRect {
@ -427,19 +889,21 @@ final class EntityKeyboardTopPanelComponent: Component {
}
func contentFrame(at index: Int) -> CGRect {
return self.contentFrame(containerFrame: self.containerFrame(at: index))
return self.items[index].innerFrame
}
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
minVisibleColumn = max(0, minVisibleColumn)
let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
let minVisibleIndex = minVisibleColumn
let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1)
return (minVisibleIndex, maxVisibleIndex)
for i in 0 ..< self.items.count {
if self.items[i].frame.intersects(rect) {
for j in i ..< self.items.count {
if !self.items[j].frame.intersects(rect) {
return (i, j - 1)
}
}
return (i, self.items.count - 1)
}
}
return (0, -1)
}
}
@ -447,10 +911,20 @@ final class EntityKeyboardTopPanelComponent: Component {
private var itemViews: [AnyHashable: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>] = [:]
private var highlightedIconBackgroundView: UIView
private var temporaryReorderingOrderIndex: (id: AnyHashable, index: Int)?
private weak var currentReorderingItemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>?
private var currentReorderingItemId: AnyHashable?
private var currentReorderingItemContainerView: UIView?
private var initialReorderingItemFrame: CGRect?
private var itemLayout: ItemLayout?
private var items: [Item] = []
private var ignoreScrolling: Bool = false
private var isDragging: Bool = false
private var isReordering: Bool = false
private var isDraggingOrReordering: Bool = false
private var draggingStoppedTimer: SwiftSignalKit.Timer?
private var isExpanded: Bool = false
@ -460,6 +934,7 @@ final class EntityKeyboardTopPanelComponent: Component {
private var activeContentItemId: AnyHashable?
private var component: EntityKeyboardTopPanelComponent?
weak var state: EmptyComponentState?
private var environment: EntityKeyboardTopContainerPanelEnvironment?
override init(frame: CGRect) {
@ -467,8 +942,8 @@ final class EntityKeyboardTopPanelComponent: Component {
self.highlightedIconBackgroundView = UIView()
self.highlightedIconBackgroundView.isUserInteractionEnabled = false
self.highlightedIconBackgroundView.layer.cornerRadius = 10.0
self.highlightedIconBackgroundView.clipsToBounds = true
self.highlightedIconBackgroundView.isHidden = true
super.init(frame: frame)
@ -483,6 +958,7 @@ final class EntityKeyboardTopPanelComponent: Component {
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = true
self.scrollView.delegate = self
self.addSubview(self.scrollView)
@ -496,6 +972,50 @@ final class EntityKeyboardTopPanelComponent: Component {
}
return strongSelf.scrollView.contentOffset.x > 0.0
}
self.addGestureRecognizer(ReorderGestureRecognizer(
shouldBegin: { [weak self] point in
guard let strongSelf = self else {
return (false, false, nil)
}
if !strongSelf.isExpanded {
return (false, false, nil)
}
let scrollViewLocation = strongSelf.convert(point, to: strongSelf.scrollView)
for (id, itemView) in strongSelf.itemViews {
if itemView.frame.contains(scrollViewLocation) {
for item in strongSelf.items {
if item.id == id, item.isReorderable {
return (true, true, itemView)
}
}
break
}
}
return (false, false, nil)
}, willBegin: { _ in
}, began: { [weak self] itemView in
guard let strongSelf = self else {
return
}
strongSelf.beginReordering(itemView: itemView)
}, ended: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.endReordering()
}, moved: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.updateReordering(offset: value)
}, isActiveUpdated: { [weak self] isActive in
guard let strongSelf = self else {
return
}
strongSelf.updateIsReordering(isActive)
}
))
}
required init?(coder: NSCoder) {
@ -525,8 +1045,20 @@ final class EntityKeyboardTopPanelComponent: Component {
}
private func updateIsDragging(_ isDragging: Bool) {
if !isDragging {
if !self.isDragging {
self.isDragging = isDragging
self.updateIsDraggingOrReordering()
}
private func updateIsReordering(_ isReordering: Bool) {
self.isReordering = isReordering
self.updateIsDraggingOrReordering()
}
private func updateIsDraggingOrReordering() {
let isDraggingOrReordering = self.isDragging || self.isReordering
if !isDraggingOrReordering {
if !self.isDraggingOrReordering {
return
}
@ -536,7 +1068,7 @@ final class EntityKeyboardTopPanelComponent: Component {
return
}
strongSelf.draggingStoppedTimer = nil
strongSelf.isDragging = false
strongSelf.isDraggingOrReordering = false
guard let environment = strongSelf.environment else {
return
}
@ -548,8 +1080,8 @@ final class EntityKeyboardTopPanelComponent: Component {
self.draggingStoppedTimer?.invalidate()
self.draggingStoppedTimer = nil
if !self.isDragging {
self.isDragging = true
if !self.isDraggingOrReordering {
self.isDraggingOrReordering = true
guard let environment = self.environment else {
return
@ -559,8 +1091,91 @@ final class EntityKeyboardTopPanelComponent: Component {
}
}
private func beginReordering(itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>) {
if let currentReorderingItemView = self.currentReorderingItemView {
if let componentView = currentReorderingItemView.componentView {
currentReorderingItemView.addSubview(componentView)
}
self.currentReorderingItemView = nil
self.currentReorderingItemId = nil
}
guard let id = self.itemViews.first(where: { $0.value === itemView })?.key else {
return
}
self.currentReorderingItemId = id
self.currentReorderingItemView = itemView
let reorderingItemContainerView: UIView
if let current = self.currentReorderingItemContainerView {
reorderingItemContainerView = current
} else {
reorderingItemContainerView = UIView()
self.addSubview(reorderingItemContainerView)
self.currentReorderingItemContainerView = reorderingItemContainerView
}
reorderingItemContainerView.alpha = 0.5
reorderingItemContainerView.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.2)
reorderingItemContainerView.frame = itemView.convert(itemView.bounds, to: self)
self.initialReorderingItemFrame = reorderingItemContainerView.frame
if let componentView = itemView.componentView {
reorderingItemContainerView.addSubview(componentView)
}
}
private func endReordering() {
if let currentReorderingItemView = self.currentReorderingItemView {
self.currentReorderingItemView = nil
if let componentView = currentReorderingItemView.componentView {
let localFrame = componentView.convert(componentView.bounds, to: self.scrollView)
currentReorderingItemView.superview?.bringSubviewToFront(currentReorderingItemView)
currentReorderingItemView.addSubview(componentView)
let deltaPosition = CGPoint(x: localFrame.minX - currentReorderingItemView.frame.minX, y: localFrame.minY - currentReorderingItemView.frame.minY)
currentReorderingItemView.layer.animatePosition(from: deltaPosition, to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
if let reorderingItemContainerView = self.currentReorderingItemContainerView {
self.currentReorderingItemContainerView = nil
reorderingItemContainerView.removeFromSuperview()
}
self.currentReorderingItemId = nil
self.temporaryReorderingOrderIndex = nil
self.component?.reorderItems(self.items)
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
private func updateReordering(offset: CGFloat) {
guard let itemLayout = self.itemLayout, let currentReorderingItemId = self.currentReorderingItemId, let reorderingItemContainerView = self.currentReorderingItemContainerView, let initialReorderingItemFrame = self.initialReorderingItemFrame else {
return
}
reorderingItemContainerView.frame = initialReorderingItemFrame.offsetBy(dx: offset, dy: 0.0)
for i in 0 ..< self.items.count {
if !self.items[i].isReorderable {
continue
}
let containerFrame = itemLayout.containerFrame(at: i)
if containerFrame.intersects(reorderingItemContainerView.frame) {
let temporaryReorderingOrderIndex: (id: AnyHashable, index: Int) = (currentReorderingItemId, i)
if self.temporaryReorderingOrderIndex?.id != temporaryReorderingOrderIndex.id || self.temporaryReorderingOrderIndex?.index != temporaryReorderingOrderIndex.index {
self.temporaryReorderingOrderIndex = temporaryReorderingOrderIndex
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
break
}
}
}
private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) {
guard let component = self.component, let itemLayout = self.itemLayout else {
guard let itemLayout = self.itemLayout else {
return
}
@ -569,9 +1184,9 @@ final class EntityKeyboardTopPanelComponent: Component {
var validIds = Set<AnyHashable>()
let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds)
if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex {
if !self.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex {
for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex {
let item = component.items[index]
let item = self.items[index]
validIds.insert(item.id)
var itemTransition = transition
@ -590,7 +1205,7 @@ final class EntityKeyboardTopPanelComponent: Component {
transition: itemTransition,
component: item.content,
environment: {
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded)
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id)
},
containerSize: itemOuterFrame.size
)
@ -618,6 +1233,7 @@ final class EntityKeyboardTopPanelComponent: Component {
self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor
}
self.component = component
self.state = state
if let defaultActiveItemId = component.defaultActiveItemId {
self.activeContentItemId = defaultActiveItemId
@ -630,11 +1246,49 @@ final class EntityKeyboardTopPanelComponent: Component {
let wasExpanded = self.isExpanded
self.isExpanded = isExpanded
if !isExpanded {
if self.isDragging {
self.isDragging = false
}
if self.isReordering {
self.isReordering = false
}
if self.isDraggingOrReordering {
self.isDraggingOrReordering = false
}
if let draggingStoppedTimer = self.draggingStoppedTimer {
self.draggingStoppedTimer = nil
draggingStoppedTimer.invalidate()
}
}
let intrinsicHeight: CGFloat = availableSize.height
let height = intrinsicHeight
var items = component.items
if let (id, index) = self.temporaryReorderingOrderIndex {
for i in 0 ..< items.count {
if items[i].id == id {
let item = items.remove(at: i)
items.insert(item, at: min(index, items.count))
break
}
}
}
self.items = items
if self.activeContentItemId == nil {
self.activeContentItemId = items.first?.id
}
let previousItemLayout = self.itemLayout
let itemLayout = ItemLayout(itemCount: component.items.count, isExpanded: isExpanded, height: availableSize.height)
let itemLayout = ItemLayout(isExpanded: isExpanded, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in
let isStatic = item.id == AnyHashable("static")
return ItemLayout.ItemDescription(
isStatic: isStatic,
isStaticExpanded: isStatic && self.activeContentItemId == item.id
)
})
self.itemLayout = itemLayout
self.ignoreScrolling = true
@ -660,7 +1314,7 @@ final class EntityKeyboardTopPanelComponent: Component {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ..< updatedVisibleRange.maxIndex {
let indexDifference = index - previousVisibleRange.minIndex
if let itemView = self.itemViews[component.items[index].id] {
if let itemView = self.itemViews[self.items[index].id] {
let itemContainerOriginX = baseFrame.minX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: baseFrame.minY), size: baseFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(containerFrame: itemContainerFrame)
@ -685,11 +1339,23 @@ final class EntityKeyboardTopPanelComponent: Component {
self.updateVisibleItems(attemptSynchronousLoads: !(self.scrollView.isDragging || self.scrollView.isDecelerating), transition: transition)
if let activeContentItemId = self.activeContentItemId {
if let index = component.items.firstIndex(where: { $0.id == activeContentItemId }) {
if let index = self.items.firstIndex(where: { $0.id == activeContentItemId }) {
let itemFrame = itemLayout.containerFrame(at: index)
transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
var highlightTransition = transition
if self.highlightedIconBackgroundView.isHidden {
self.highlightedIconBackgroundView.isHidden = false
highlightTransition = .immediate
}
highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: activeContentItemId.base is String ? min(itemFrame.width / 2.0, itemFrame.height / 2.0) : 10.0)
highlightTransition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
highlightTransition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
} else {
self.highlightedIconBackgroundView.isHidden = true
}
} else {
self.highlightedIconBackgroundView.isHidden = true
}
transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0)
@ -732,17 +1398,28 @@ final class EntityKeyboardTopPanelComponent: Component {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
if self.activeContentItemId == itemId {
return
}
self.activeContentItemId = itemId
let _ = component
let _ = itemLayout
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
var found = false
for i in 0 ..< component.items.count {
if component.items[i].id == itemId {
/*var found = false
for i in 0 ..< self.items.count {
if self.items[i].id == itemId {
found = true
self.highlightedIconBackgroundView.isHidden = false
let itemFrame = itemLayout.containerFrame(at: i)
transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
var highlightTransition = transition
if highlightTransition.animation.isImmediate {
highlightTransition = highlightTransition.withAnimation(.curve(duration: 0.3, curve: .spring))
}
highlightTransition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
highlightTransition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -6.0, dy: 0.0), animated: true)
@ -751,7 +1428,7 @@ final class EntityKeyboardTopPanelComponent: Component {
}
if !found {
self.highlightedIconBackgroundView.isHidden = true
}
}*/
}
}

View file

@ -18,6 +18,7 @@ import PagerComponent
import SoftwareVideo
import AVFoundation
import PhotoResources
import ContextUI
private class GifVideoLayer: AVSampleBufferDisplayLayer {
private let context: AccountContext
@ -125,11 +126,14 @@ public final class GifPagerContentComponent: Component {
public final class InputInteraction {
public let performItemAction: (Item, UIView, CGRect) -> Void
public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
public init(
performItemAction: @escaping (Item, UIView, CGRect) -> Void
performItemAction: @escaping (Item, UIView, CGRect) -> Void,
openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
) {
self.performItemAction = performItemAction
self.openGifContextMenu = openGifContextMenu
}
}
@ -186,7 +190,7 @@ public final class GifPagerContentComponent: Component {
return true
}
public final class View: UIView, UIScrollViewDelegate {
public final class View: ContextControllerSourceView, UIScrollViewDelegate {
private struct ItemGroupDescription: Equatable {
let hasTitle: Bool
let itemCount: Int
@ -406,12 +410,44 @@ public final class GifPagerContentComponent: Component {
self.addSubview(self.scrollView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.useSublayerTransformForActivation = false
self.shouldBegin = { [weak self] point in
guard let strongSelf = self else {
return false
}
strongSelf.targetLayerForActivationProgress = nil
if let (_, itemLayer) = strongSelf.itemLayer(atPoint: point) {
strongSelf.targetLayerForActivationProgress = itemLayer
return true
}
return false
}
self.activated = { [weak self] gesture, location in
guard let strongSelf = self, let component = strongSelf.component else {
gesture.cancel()
return
}
guard let (item, itemLayer) = strongSelf.itemLayer(atPoint: location) else {
gesture.cancel()
return
}
let rect = strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf)
component.inputInteraction.openGifContextMenu(item.file, strongSelf, rect, gesture, true)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
guard let component = self.component else {
return
}
component.inputInteraction.openGifContextMenu(file, sourceView, sourceRect, gesture, isSaved)
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] {
@ -432,6 +468,18 @@ public final class GifPagerContentComponent: Component {
return nil
}
private func itemLayer(atPoint point: CGPoint) -> (Item, ItemLayer)? {
let localPoint = self.convert(point, to: self.scrollView)
for (_, itemLayer) in self.visibleItemLayers {
if itemLayer.frame.contains(localPoint) {
return (itemLayer.item, itemLayer)
}
}
return nil
}
private var previousScrollingOffset: CGFloat?
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
@ -600,4 +648,3 @@ public final class GifPagerContentComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -15,6 +15,7 @@ swift_library(
"//submodules/Display:Display",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
"//submodules/WebPBinding:WebPBinding",
],
visibility = [
"//visibility:public",

View file

@ -4,6 +4,7 @@ import AnimationCache
import Display
import RLottieBinding
import GZip
import WebPBinding
public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) {
writer.queue.async {
@ -26,3 +27,21 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: An
writer.finish()
}
}
public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) {
writer.queue.async {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) {
writer.add(with: { surface in
let context = DrawingContext(size: CGSize(width: CGFloat(surface.width), height: CGFloat(surface.height)), scale: 1.0, opaque: false, clear: true, bytesPerRow: surface.bytesPerRow)
context.withContext { c in
UIGraphicsPushContext(c)
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size))
UIGraphicsPopContext()
}
memcpy(surface.argb, context.bytes, surface.height * surface.bytesPerRow)
}, proposedWidth: width, proposedHeight: height, duration: 1.0)
}
writer.finish()
}
}

View file

@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "MultiAnimationRendererMetalResources",
srcs = glob([
"Resources/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "WallpaperBackgroundNodeBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.MultiAnimationRenderer</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>MultiAnimationRenderer</string>
"""
)
apple_resource_bundle(
name = "MultiAnimationRendererBundle",
infoplists = [
":WallpaperBackgroundNodeBundleInfoPlist",
],
resources = [
":MultiAnimationRendererMetalResources",
],
)
swift_library(
name = "MultiAnimationRenderer",
@ -6,6 +46,9 @@ swift_library(
srcs = glob([
"Sources/**/*.swift",
]),
data = [
":MultiAnimationRendererBundle",
],
copts = [
"-warnings-as-errors",
],

View file

@ -0,0 +1,38 @@
#include <metal_stdlib>
using namespace metal;
typedef struct {
packed_float2 position;
packed_float2 texCoord;
} Vertex;
typedef struct {
float4 position[[position]];
float2 texCoord;
} Varyings;
vertex Varyings multiAnimationVertex(
unsigned int vid[[vertex_id]],
constant Vertex *verticies[[buffer(0)]],
constant uint2 &resolution[[buffer(1)]],
constant uint2 &slotSize[[buffer(2)]],
constant uint2 &slotPosition[[buffer(3)]]
) {
Varyings out;
constant Vertex &v = verticies[vid];
out.position = float4(float2(v.position), 0.0, 1.0);
out.texCoord = v.texCoord;
return out;
}
fragment half4 multiAnimationFragment(
Varyings in[[stage_in]],
texture2d<float, access::sample> texture[[texture(0)]]
) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
return half4(texture.sample(s, in.texCoord));
}

View file

@ -0,0 +1,767 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AnimationCache
import Accelerate
import simd
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
private extension Float {
func remap(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float) -> Float {
guard (fromHigh - fromLow) != 0.0 else {
return 0.0
}
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
}
private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? {
guard let loadedVertexProgram = library.makeFunction(name: vertexProgram) else {
return nil
}
guard let loadedFragmentProgram = library.makeFunction(name: fragmentProgram) else {
return nil
}
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else {
return nil
}
return pipelineState
}
@available(iOS 13.0, *)
public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
private final class LoadFrameTask {
let task: () -> () -> Void
init(task: @escaping () -> () -> Void) {
self.task = task
}
}
private final class TargetReference {
let id: Int64
weak var value: MultiAnimationRenderTarget?
init(_ value: MultiAnimationRenderTarget) {
self.value = value
self.id = value.id
}
}
private final class TextureStoragePool {
let width: Int
let height: Int
private var items: [TextureStorage.Content] = []
init(width: Int, height: Int) {
self.width = width
self.height = height
}
func recycle(content: TextureStorage.Content) {
if self.items.count < 4 {
self.items.append(content)
} else {
print("Warning: over-recycling texture storage")
}
}
func take(device: MTLDevice) -> TextureStorage.Content? {
if self.items.isEmpty {
guard let content = TextureStorage.Content(device: device, width: self.width, height: self.height) else {
return nil
}
return content
}
return self.items.removeLast()
}
}
private final class TextureStorage {
final class Content {
let buffer: MTLBuffer?
let width: Int
let height: Int
let bytesPerRow: Int
let texture: MTLTexture
init?(device: MTLDevice, width: Int, height: Int) {
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
#if targetEnvironment(simulator)
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
self.buffer = nil
#else
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
return nil
}
self.buffer = buffer
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = buffer.storageMode
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
return nil
}
#endif
self.texture = texture
}
func replace(rgbaData: Data, range: Range<Int>, width: Int, height: Int, bytesPerRow: Int) {
if width != self.width || height != self.height {
assert(false, "Image size does not match")
return
}
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1))
if let buffer = self.buffer, self.bytesPerRow == bytesPerRow {
rgbaData.withUnsafeBytes { bytes in
let _ = memcpy(buffer.contents(), bytes.baseAddress!.advanced(by: range.lowerBound), bytesPerRow * height)
}
} else {
rgbaData.withUnsafeBytes { bytes in
self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!.advanced(by: range.lowerBound), bytesPerRow: bytesPerRow)
}
}
}
}
private weak var pool: TextureStoragePool?
let content: Content
private var isInvalidated: Bool = false
init(pool: TextureStoragePool, content: Content) {
self.pool = pool
self.content = content
}
deinit {
if !self.isInvalidated {
self.pool?.recycle(content: self.content)
}
}
/*func createCGImage() -> CGImage? {
if self.isInvalidated {
return nil
}
self.isInvalidated = true
#if targetEnvironment(simulator)
guard let data = NSMutableData(capacity: self.content.bytesPerRow * self.content.height) else {
return nil
}
data.length = self.content.bytesPerRow * self.content.height
self.content.texture.getBytes(data.mutableBytes, bytesPerRow: self.content.bytesPerRow, bytesPerImage: self.content.bytesPerRow * self.content.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.content.width, height: self.content.height, depth: 1)), mipmapLevel: 0, slice: 0)
guard let dataProvider = CGDataProvider(data: data as CFData) else {
return nil
}
#else
let content = self.content
let pool = self.pool
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.content.buffer.contents(), count: self.content.buffer.length, deallocator: .custom { [weak pool] _, _ in
guard let pool = pool else {
return
}
pool.recycle(content: content)
}) as CFData) else {
return nil
}
#endif
guard let image = CGImage(
width: Int(self.content.width),
height: Int(self.content.height),
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.content.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return image
}*/
}
private final class Frame {
let timestamp: Double
let texture: TextureStorage.Content
init(device: MTLDevice, texture: TextureStorage.Content, data: AnimationCacheItemFrame, timestamp: Double) {
self.timestamp = timestamp
self.texture = texture
switch data.format {
case let .rgba(width, height, bytesPerRow):
texture.replace(rgbaData: data.data, range: data.range, width: width, height: height, bytesPerRow: bytesPerRow)
}
}
}
private final class ItemContext {
static let queue = Queue(name: "MultiAnimationMetalRendererImpl", qos: .default)
private let cache: AnimationCache
private let stateUpdated: () -> Void
private var disposable: Disposable?
private var timestamp: Double = 0.0
private var item: AnimationCacheItem?
private(set) var currentFrame: Frame?
private var isLoadingFrame: Bool = false
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
var targets: [TargetReference] = []
var slotIndex: Int
init(slotIndex: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
self.slotIndex = slotIndex
self.cache = cache
self.stateUpdated = stateUpdated
self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.item = result.item
strongSelf.updateIsPlaying()
if result.item == nil {
for target in strongSelf.targets {
if let target = target.value {
target.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
}
}
})
}
deinit {
self.disposable?.dispose()
}
func updateIsPlaying() {
var isPlaying = true
if self.item == nil {
isPlaying = false
}
var shouldBeAnimating = false
for target in self.targets {
if let target = target.value {
if target.shouldBeAnimating {
shouldBeAnimating = true
break
}
}
}
if !shouldBeAnimating {
isPlaying = false
}
self.isPlaying = isPlaying
}
func animationTick(device: MTLDevice, texturePool: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? {
return self.update(device: device, texturePool: texturePool, advanceTimestamp: advanceTimestamp)
}
private func update(device: MTLDevice, texturePool: TextureStoragePool, advanceTimestamp: Double?) -> LoadFrameTask? {
guard let item = self.item else {
return nil
}
let timestamp = self.timestamp
if let advanceTimestamp = advanceTimestamp {
self.timestamp += advanceTimestamp
}
if let currentFrame = self.currentFrame, currentFrame.timestamp == self.timestamp {
} else if !self.isLoadingFrame {
self.isLoadingFrame = true
return LoadFrameTask(task: { [weak self] in
let frame = item.getFrame(at: timestamp)
return {
guard let strongSelf = self else {
return
}
var currentFrame: Frame?
let texture = texturePool.take(device: device)
if let frame = frame, let texture = texture {
currentFrame = Frame(device: device, texture: texture, data: frame, timestamp: timestamp)
}
strongSelf.isLoadingFrame = false
if let currentFrame = currentFrame {
strongSelf.currentFrame = currentFrame
}
}
})
}
return nil
}
}
private final class SurfaceLayer: CAMetalLayer {
private let cellSize: CGSize
private let stateUpdated: () -> Void
private let metalDevice: MTLDevice
private let commandQueue: MTLCommandQueue
private let renderPipelineState: MTLRenderPipelineState
private let texturePool: TextureStoragePool
private let slotCount: Int
private let slotsX: Int
private let slotsY: Int
private var itemContexts: [String: ItemContext] = [:]
private var slotToItemId: [String?]
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
public init(cellSize: CGSize, stateUpdated: @escaping () -> Void) {
self.cellSize = cellSize
self.stateUpdated = stateUpdated
self.slotsX = 16
self.slotsY = 16
let drawableSize = CGSize(width: cellSize.width * CGFloat(self.slotsX), height: cellSize.height * CGFloat(self.slotsY))
self.slotCount = (Int(drawableSize.width) / Int(cellSize.width)) * (Int(drawableSize.height) / Int(cellSize.height))
self.slotToItemId = (0 ..< self.slotCount).map { _ in nil }
self.metalDevice = MTLCreateSystemDefaultDevice()!
self.commandQueue = self.metalDevice.makeCommandQueue()!
let mainBundle = Bundle(for: MultiAnimationMetalRendererImpl.self)
guard let path = mainBundle.path(forResource: "MultiAnimationRendererBundle", ofType: "bundle") else {
preconditionFailure()
}
guard let bundle = Bundle(path: path) else {
preconditionFailure()
}
guard let defaultLibrary = try? self.metalDevice.makeDefaultLibrary(bundle: bundle) else {
preconditionFailure()
}
self.renderPipelineState = makePipelineState(device: self.metalDevice, library: defaultLibrary, vertexProgram: "multiAnimationVertex", fragmentProgram: "multiAnimationFragment")!
self.texturePool = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height))
super.init()
self.device = self.metalDevice
self.maximumDrawableCount = 2
//self.metalLayer.presentsWithTransaction = true
self.contentsScale = 1.0
self.drawableSize = drawableSize
self.pixelFormat = .bgra8Unorm
self.framebufferOnly = true
self.allowsNextDrawableTimeout = true
}
override public init(layer: Any) {
preconditionFailure()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func action(forKey event: String) -> CAAction? {
return nullAction
}
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable? {
if size != self.cellSize {
return nil
}
let targetId = target.id
if self.itemContexts[itemId] == nil {
for i in 0 ..< self.slotCount {
if self.slotToItemId[i] == nil {
self.slotToItemId[i] = itemId
self.itemContexts[itemId] = ItemContext(slotIndex: i, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
break
}
}
}
if let itemContext = self.itemContexts[itemId] {
itemContext.targets.append(TargetReference(target))
target.contents = self.contents
let slotX = itemContext.slotIndex % self.slotsX
let slotY = itemContext.slotIndex / self.slotsX
let totalX = CGFloat(self.slotsX) * self.cellSize.width
let totalY = CGFloat(self.slotsY) * self.cellSize.height
let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY))
target.contentsRect = contentsRect
self.isPlaying = true
return ActionDisposable { [weak self, weak itemContext] in
Queue.mainQueue().async {
guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else {
return
}
if let index = currentItemContext.targets.firstIndex(where: { $0.id == targetId }) {
currentItemContext.targets.remove(at: index)
if currentItemContext.targets.isEmpty {
strongSelf.slotToItemId[currentItemContext.slotIndex] = nil
strongSelf.itemContexts.removeValue(forKey: itemId)
if strongSelf.itemContexts.isEmpty {
strongSelf.isPlaying = false
}
}
}
}
}
} else {
return nil
}
}
private func updateIsPlaying() {
var isPlaying = false
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
func animationTick(advanceTimestamp: Double) -> [LoadFrameTask] {
var tasks: [LoadFrameTask] = []
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
if let task = itemContext.animationTick(device: self.metalDevice, texturePool: self.texturePool, advanceTimestamp: advanceTimestamp) {
tasks.append(task)
}
}
}
return tasks
}
func redraw() {
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
guard let drawable = self.nextDrawable() else {
return
}
/*let drawTime = CACurrentMediaTime() - timestamp
if drawTime > 9.0 / 1000.0 {
print("get time \(drawTime * 1000.0)")
}*/
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: 0.0
)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
var usedTextures: [MultiAnimationMetalRendererImpl.TextureStorage.Content] = []
var vertices: [Float] = [
-1.0, -1.0, 0.0, 0.0,
1.0, -1.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 1.0,
1.0, 1.0, 1.0, 1.0
]
renderEncoder.setRenderPipelineState(self.renderPipelineState)
var resolution = simd_uint2(UInt32(drawable.texture.width), UInt32(drawable.texture.height))
renderEncoder.setVertexBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 1)
var slotSize = simd_uint2(UInt32(self.cellSize.width), UInt32(self.cellSize.height))
renderEncoder.setVertexBytes(&slotSize, length: MemoryLayout<simd_uint2>.size * 2, index: 2)
for (_, itemContext) in self.itemContexts {
guard let frame = itemContext.currentFrame else {
continue
}
let slotX = itemContext.slotIndex % self.slotsX
let slotY = self.slotsY - 1 - itemContext.slotIndex / self.slotsY
let totalX = CGFloat(self.slotsX) * self.cellSize.width
let totalY = CGFloat(self.slotsY) * self.cellSize.height
let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY))
vertices[4 * 0 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 0 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 1 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 1 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 2 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 2 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 3 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
vertices[4 * 3 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0)
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
var slotPosition = simd_uint2(UInt32(itemContext.slotIndex % self.slotsX), UInt32(itemContext.slotIndex % self.slotsY))
renderEncoder.setVertexBytes(&slotPosition, length: MemoryLayout<simd_uint2>.size * 2, index: 3)
usedTextures.append(frame.texture)
renderEncoder.setFragmentTexture(frame.texture.texture, index: 0)
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1)
}
renderEncoder.endEncoding()
if self.presentsWithTransaction {
if Thread.isMainThread {
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
drawable.present()
} else {
CATransaction.begin()
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
drawable.present()
CATransaction.commit()
}
} else {
commandBuffer.addScheduledHandler { _ in
drawable.present()
}
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
for _ in usedTextures {
}
}
}
commandBuffer.commit()
}
}
}
private var nextSurfaceLayerIndex: Int = 1
private var surfaceLayers: [Int: SurfaceLayer] = [:]
private var frameSkip: Int
private var displayLink: ConstantDisplayLinkAnimator?
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
if self.isPlaying {
if self.displayLink == nil {
self.displayLink = ConstantDisplayLinkAnimator { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationTick()
}
self.displayLink?.frameInterval = self.frameSkip
self.displayLink?.isPaused = false
}
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
}
}
public init() {
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 {
self.frameSkip = 1
} else {
self.frameSkip = 2
}
}
private func updateIsPlaying() {
var isPlaying = false
for (_, surfaceLayer) in self.surfaceLayers {
if surfaceLayer.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
assert(Thread.isMainThread)
let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16)))
for (_, surfaceLayer) in self.surfaceLayers {
if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) {
return disposable
}
}
let index = self.nextSurfaceLayerIndex
self.nextSurfaceLayerIndex += 1
let surfaceLayer = SurfaceLayer(cellSize: alignedSize, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.surfaceLayers[index] = surfaceLayer
if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) {
return disposable
} else {
return EmptyDisposable
}
}
public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
return false
}
public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
completion(false)
return EmptyDisposable
}
private func animationTick() {
let secondsPerFrame = Double(self.frameSkip) / 60.0
var tasks: [LoadFrameTask] = []
var surfaceLayersWithTasks: [Int] = []
for (index, surfaceLayer) in self.surfaceLayers {
var hasTasks = false
if surfaceLayer.isPlaying {
let surfaceLayerTasks = surfaceLayer.animationTick(advanceTimestamp: secondsPerFrame)
if !surfaceLayerTasks.isEmpty {
tasks.append(contentsOf: surfaceLayerTasks)
hasTasks = true
}
}
if hasTasks {
surfaceLayersWithTasks.append(index)
}
}
if !tasks.isEmpty {
ItemContext.queue.async { [weak self] in
var completions: [() -> Void] = []
for task in tasks {
let complete = task.task()
completions.append(complete)
}
if !completions.isEmpty {
Queue.mainQueue().async {
for completion in completions {
completion()
}
}
}
if let strongSelf = self {
for index in surfaceLayersWithTasks {
if let surfaceLayer = strongSelf.surfaceLayers[index] {
surfaceLayer.redraw()
}
}
}
}
}
}
}

View file

@ -11,7 +11,11 @@ public protocol MultiAnimationRenderer: AnyObject {
func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
}
private var nextRenderTargetId: Int64 = 1
open class MultiAnimationRenderTarget: SimpleLayer {
public let id: Int64
fileprivate let deinitCallbacks = Bag<() -> Void>()
fileprivate let updateStateCallbacks = Bag<() -> Void>()
@ -25,6 +29,29 @@ open class MultiAnimationRenderTarget: SimpleLayer {
}
}
public override init() {
assert(Thread.isMainThread)
self.id = nextRenderTargetId
nextRenderTargetId += 1
super.init()
}
public override init(layer: Any) {
guard let layer = layer as? MultiAnimationRenderTarget else {
preconditionFailure()
}
self.id = layer.id
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
for f in self.deinitCallbacks.copyItems() {
f()
@ -369,7 +396,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
}
private let firstFrameQueue: Queue
public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
private var groupContexts: [String: GroupContext] = [:]
private var frameSkip: Int
private var displayLink: ConstantDisplayLinkAnimator?
@ -399,8 +427,6 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
public init() {
self.firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 {
self.frameSkip = 1
} else {
@ -413,7 +439,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
@ -434,7 +460,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
@ -451,7 +477,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
@ -475,14 +501,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
self.isPlaying = isPlaying
}
private var previousTimestamp: Double?
private func animationTick() {
let timestamp = CFAbsoluteTimeGetCurrent()
if let _ = self.previousTimestamp {
}
self.previousTimestamp = timestamp
let secondsPerFrame = Double(self.frameSkip) / 60.0
var tasks: [LoadFrameGroupTask] = []

View file

@ -45,17 +45,20 @@ public final class TextNodeWithEntities {
public let cache: AnimationCache
public let renderer: MultiAnimationRenderer
public let placeholderColor: UIColor
public let attemptSynchronous: Bool
public init(
context: AccountContext,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
placeholderColor: UIColor
placeholderColor: UIColor,
attemptSynchronous: Bool
) {
self.context = context
self.cache = cache
self.renderer = renderer
self.placeholderColor = placeholderColor
self.attemptSynchronous = attemptSynchronous
}
}
@ -115,7 +118,7 @@ public final class TextNodeWithEntities {
if let maybeNode = maybeNode {
if let applyArguments = applyArguments {
maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor)
maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false)
}
return maybeNode
@ -123,7 +126,7 @@ public final class TextNodeWithEntities {
let resultNode = TextNodeWithEntities(textNode: result)
if let applyArguments = applyArguments {
resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor)
resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false)
}
return resultNode
@ -140,7 +143,7 @@ public final class TextNodeWithEntities {
}
}
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) {
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool) {
var nextIndexById: [Int64: Int] = [:]
var validIds: [InlineStickerItemLayer.Key] = []
@ -165,7 +168,7 @@ public final class TextNodeWithEntities {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
self.inlineStickerItemLayers[id] = itemLayer
self.textNode.layer.addSublayer(itemLayer)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"flag 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 76","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[5]},{"t":20,"s":[0]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[6]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[6]},{"t":20,"s":[6]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[9.333]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[17.833]},{"t":20,"s":[17]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,9],[0,-9]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":0,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 76","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Rectangle 155","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[54,-33,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-4.417],[-9,-6.583],[-9,4.417],[-4,6.583],[4,4.417],[9,6.583],[9,-4.417],[4,-6.583]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-6.5],[-9,-4.5],[-9,6.5],[-4,4.5],[4,6.5],[9,4.5],[9,-6.5],[4,-4.5]],"c":true}]},{"t":30,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-4.417],[-9,-6.583],[-9,4.417],[-4,6.583],[4,4.417],[9,6.583],[9,-4.417],[4,-6.583]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 155","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👋 🤚 🖐 ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👣 👂 🦻 👃 🫀 🫁 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸 👶 👧 🧒 👦 👩 🧑 👨 👩‍🦱 🧑‍🦱 👨‍🦱 👩‍🦰 🧑‍🦰 👨‍🦰 👱‍♀️ 👱 👱‍♂️ 👩‍🦳 🧑‍🦳 👨‍🦳 👩‍🦲 🧑‍🦲 👨‍🦲 🧔 👵 🧓 👴 👲 👳‍♀️ 👳 👳‍♂️ 🧕 👮‍♀️ 👮 👮‍♂️ 👷‍♀️ 👷 👷‍♂️ 💂‍♀️ 💂 💂‍♂️ 🕵️‍♀️ 🕵️ 🕵️‍♂️ 👩‍⚕️ 🧑‍⚕️ 👨‍⚕️ 👩‍🌾 🧑‍🌾 👨‍🌾 👩‍🍳 🧑‍🍳 👨‍🍳 👩‍🎓 🧑‍🎓 👨‍🎓 👩‍🎤 🧑‍🎤 👨‍🎤 👩‍🏫 🧑‍🏫 👨‍🏫 👩‍🏭 🧑‍🏭 👨‍🏭 👩‍💻 🧑‍💻 👨‍💻 👩‍💼 🧑‍💼 👨‍💼 👩‍🔧 🧑‍🔧 👨‍🔧 👩‍🔬 🧑‍🔬 👨‍🔬 👩‍🎨 🧑‍🎨 👨‍🎨 👩‍🚒 🧑‍🚒 👨‍🚒 👩‍✈️ 🧑‍✈️ 👨‍✈️ 👩‍🚀 🧑‍🚀 👨‍🚀 👩‍⚖️ 🧑‍⚖️ 👨‍⚖️ 👰‍♀️ 👰 👰‍♂️ 🤵‍♀️ 🤵 🤵‍♂️ 👸 🤴 🥷 🦸‍♀️ 🦸 🦸‍♂️ 🦹‍♀️ 🦹 🦹‍♂️ 🤶 🧑‍🎄 🎅 🧙‍♀️ 🧙 🧙‍♂️ 🧝‍♀️ 🧝 🧝‍♂️ 🧛‍♀️ 🧛 🧛‍♂️ 🧟‍♀️ 🧟 🧟‍♂️ 🧞‍♀️ 🧞 🧞‍♂️ 🧜‍♀️ 🧜 🧜‍♂️ 🧚‍♀️ 🧚 🧚‍♂️ 👼 🤰 🤱 👩‍🍼 🧑‍🍼 👨‍🍼 🙇‍♀️ 🙇 🙇‍♂️ 💁‍♀️ 💁 💁‍♂️ 🙅‍♀️ 🙅 🙅‍♂️ 🙆‍♀️ 🙆 🙆‍♂️ 🙋‍♀️ 🙋 🙋‍♂️ 🧏‍♀️ 🧏 🧏‍♂️ 🤦‍♀️ 🤦 🤦‍♂️ 🤷‍♀️ 🤷 🤷‍♂️ 🙎‍♀️ 🙎 🙎‍♂️ 🙍‍♀️ 🙍 🙍‍♂️ 💇‍♀️ 💇 💇‍♂️ 💆‍♀️ 💆 💆‍♂️ 🧖‍♀️ 🧖 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯 👯‍♂️ 🕴 👩‍🦽 🧑‍🦽 👨‍🦽 👩‍🦼 🧑‍🦼 👨‍🦼 🚶‍♀️ 🚶 🚶‍♂️ 👩‍🦯 🧑‍🦯 👨‍🦯 🧎‍♀️ 🧎 🧎‍♂️ 🏃‍♀️ 🏃 🏃‍♂️ 🧍‍♀️ 🧍 🧍‍♂️ 👭 🧑‍🤝‍🧑 👬 👫 👩‍❤️‍👩 💑 👨‍❤️‍👨 👩‍❤️‍👨 👩‍❤️‍💋‍👩 💏 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👨 👪 👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👦 👨‍👦‍👦 👨‍👧 👨‍👧‍👦 👨‍👧‍👧 👩‍👦 👩‍👦‍👦 👩‍👧 👩‍👧‍👦 👩‍👧‍👧 🗣 👤 👥 🫂 🧳 🌂 ☂️ 🧵 🪡 🪢 🧶 👓 🕶 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩴 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩰 👢 👑 👒 🎩 🎓 🧢 ⛑ 🪖 💄 💍 💼
🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻‍❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷 🕸 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🦬 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕‍🦺 🐈 🐈‍⬛ 🪶 🐓 🦃 🦤 🦚 🦜 🦢 🦩 🕊 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🪵 🌱 🌿 ☘️ 🍀 🎍 🪴 🎋 🍃 🍂 🍁 🍄 🐚 🪨 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 🪐 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫
🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🥯 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 🫖 ☕️ 🍵 🧃 🥤 🧋 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧉 🍾 🧊 🥄 🍴 🍽 🥣 🥡 🥢 🧂
⚽️ 🏀 🏈 ⚾️ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🪃 🥅 ⛳️ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸ 🥌 🎿 ⛷ 🏂 🪂 🏋️‍♀️ 🏋️ 🏋️‍♂️ 🤼‍♀️ 🤼 🤼‍♂️ 🤸‍♀️ 🤸 🤸‍♂️ ⛹️‍♀️ ⛹️ ⛹️‍♂️ 🤺 🤾‍♀️ 🤾 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏌️‍♂️ 🏇 🧘‍♀️ 🧘 🧘‍♂️ 🏄‍♀️ 🏄 🏄‍♂️ 🏊‍♀️ 🏊 🏊‍♂️ 🤽‍♀️ 🤽 🤽‍♂️ 🚣‍♀️ 🚣 🚣‍♂️ 🧗‍♀️ 🧗 🧗‍♂️ 🚵‍♀️ 🚵 🚵‍♂️ 🚴‍♀️ 🚴 🚴‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹 🤹‍♂️ 🤹‍♀️ 🎭 🩰 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🪘 🎷 🎺 🪗 🎸 🪕 🎻 🎲 ♟ 🎯 🎳 🎮 🎰 🧩
🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍 🛺 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ 🪝 ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🛖 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🛕 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁
⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 🧭 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🪔 🧯 🛢 💸 💵 💴 💶 💷 🪙 💰 💳 💎 ⚖️ 🪜 🧰 🪛 🔧 🔨 ⚒ 🛠 ⛏ 🪚 🔩 ⚙️ 🪤 🧱 ⛓ 🧲 🔫 💣 🧨 🪓 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ 🪦 ⚱️ 🏺 🔮 📿 🧿 💈 ⚗️ 🔭 🔬 🕳 🩹 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡 🧹 🪠 🧺 🧻 🚽 🚰 🚿 🛁 🛀 🧼 🪥 🪒 🧽 🪣 🧴 🛎 🔑 🗝 🚪 🪑 🛋 🛏 🛌 🧸 🪆 🖼 🪞 🪟 🛍 🛒 🎁 🎈 🎏 🎀 🪄 🪅 🎊 🎉 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 🪧 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒 🗓 📆 📅 🗑 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🧷 🔗 📎 🖇 📐 📏 🧮 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓
❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🛗 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 ⚧ 🚻 🚮 🎦 📶 🈁 🔣 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0⃣ 1⃣ 2⃣ 3⃣ 4⃣ 5⃣ 6⃣ 7⃣ 8⃣ 9⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧
🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏳️‍⚧️ 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼

View file

@ -129,7 +129,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController {
strongSelf.controllerNode.activateInput()
}
}
controller.dismissed = {
controller.dismissed = {
self?.controllerNode.activateInput()
}
strongSelf.push(controller)

View file

@ -3405,7 +3405,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).start(next: { [weak self] responded in
if let strongSelf = self {
if !responded {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start()
}
@ -8031,7 +8031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
switch result {
case .generic:
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: true, action: { _ in return false }), with: nil)
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false }), with: nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
@ -8040,7 +8040,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: true, action: { [weak self] action in
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
@ -12513,7 +12513,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).start(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView), elevatedLayout: false, action: { [weak self] action in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
let _ = strongSelf.controllerInteraction?.openMessage(message, .default)
}
@ -16138,7 +16138,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceRect = self.sourceRect
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceRect ?? sourceNode.bounds)
return (sourceNode.view, sourceRect ?? sourceNode.bounds)
} else {
return nil
}

View file

@ -2022,7 +2022,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
private let emptyInputView = UIView()
private final class EmptyInputView: UIView, UIInputViewAudioFeedback {
var enableInputClicksWhenVisible: Bool {
return true
}
}
private let emptyInputView = EmptyInputView()
private func chatPresentationInterfaceStateInputView(_ state: ChatPresentationInterfaceState) -> UIView? {
switch state.inputMode {
case .text:

View file

@ -15,6 +15,33 @@ import ComponentDisplayAdapters
import SettingsUI
import TextFormat
import PagerComponent
import AppBundle
import PremiumUI
import AudioToolbox
import UndoUI
import ContextUI
import GalleryUI
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
return []
}
guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else {
return []
}
var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = []
let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases
let segments = string.components(separatedBy: "\n\n")
for i in 0 ..< min(segments.count, orderedSegments.count) {
let list = segments[i].components(separatedBy: " ")
result.append((orderedSegments[i], list))
}
return result
}()
final class ChatEntityKeyboardInputNode: ChatInputNode {
struct InputData: Equatable {
@ -40,29 +67,70 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction] item, _, _, _ in
guard let interfaceInteraction = interfaceInteraction else {
return
}
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in item.file.attributes {
switch attribute {
case let .Sticker(displayText, packReference, _):
text = displayText
if let packReference = packReference {
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: item.file.fileId.id, file: item.file)
break loop
}
default:
break
performItemAction: { [weak interfaceInteraction, weak controllerInteraction] item, _, _, _ in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
}
if let emojiAttribute = emojiAttribute {
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
}
if let file = item.file {
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, displayText, packReference):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if file.isPremiumEmoji && !hasPremium {
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: "Subscribe to Telegram Premium to unlock this emoji.", undoText: "More", customAction: { [weak controllerInteraction] in
guard let controllerInteraction = controllerInteraction else {
return
}
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .premiumStickers, action: {
let controller = PremiumIntroScreen(context: context, source: .stickers)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
controllerInteraction.navigationController()?.pushViewController(controller)
/*let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)*/
}), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
return
}
if let emojiAttribute = emojiAttribute {
AudioServicesPlaySystemSound(0x450)
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
}
} else if let staticEmoji = item.staticEmoji {
AudioServicesPlaySystemSound(0x450)
interfaceInteraction.insertText(NSAttributedString(string: staticEmoji, attributes: [:]))
}
})
},
deleteBackwards: { [weak interfaceInteraction] in
guard let interfaceInteraction = interfaceInteraction else {
@ -72,6 +140,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
},
openStickerSettings: {
},
openPremiumSection: { [weak controllerInteraction] in
guard let controllerInteraction = controllerInteraction else {
return
}
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
},
pushController: { [weak controllerInteraction] controller in
guard let controllerInteraction = controllerInteraction else {
return
@ -103,10 +178,20 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
)
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction] item, view, rect, layer in
guard let interfaceInteraction = interfaceInteraction else {
return
}
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect, layer)
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let interfaceInteraction = interfaceInteraction else {
return
}
if let file = item.file {
if file.isPremiumSticker && !hasPremium {
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
return
}
let _ = interfaceInteraction.sendSticker(.standalone(media: file), false, view, rect, layer)
}
})
},
deleteBackwards: { [weak interfaceInteraction] in
guard let interfaceInteraction = interfaceInteraction else {
@ -122,6 +207,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
controller.navigationPresentation = .modal
controllerInteraction.navigationController()?.pushViewController(controller)
},
openPremiumSection: {
},
pushController: { [weak controllerInteraction] controller in
guard let controllerInteraction = controllerInteraction else {
return
@ -157,31 +244,52 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
},
openGifContextMenu: { _, _, _, _, _ in
}
)
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer = MultiAnimationRendererImpl()
let animationRenderer: MultiAnimationRenderer
/*if #available(iOS 13.0, *) {
animationRenderer = MultiAnimationMetalRendererImpl()
} else {*/
animationRenderer = MultiAnimationRendererImpl()
//}
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
|> map { view -> EmojiPagerContentComponent in
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var isPremium: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
var emojiCollectionIds = Set<ItemCollectionId>()
for (id, info, _) in view.collectionInfos {
if let info = info as? StickerPackCollectionInfo {
if info.shortName.lowercased().contains("emoji") {
emojiCollectionIds.insert(id)
for (subgroupId, list) in staticEmojiMapping {
let groupId: AnyHashable = "static"
for emojiString in list {
let resultItem = EmojiPagerContentComponent.Item(
file: nil,
staticEmoji: emojiString,
subgroupId: subgroupId.rawValue
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
}
}
}
@ -190,22 +298,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
guard let item = entry.item as? StickerPackItem else {
continue
}
if item.file.isAnimatedSticker || item.file.isVideoSticker {
if emojiCollectionIds.contains(entry.index.collectionId) {
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.file,
stickerPackItem: nil
)
let groupId = entry.index.collectionId
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
}
}
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = entry.index.collectionId
let groupId: AnyHashable = supergroupId
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremium && isPremiumDisabled {
continue
}
/*if isPremium {
groupId = "\(supergroupId)-p"
} else {
groupId = supergroupId
}*/
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem]))
}
}
@ -229,28 +343,21 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items)
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
},
itemLayoutType: .compact
)
}
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var displayPremiumBadges: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
@ -282,9 +389,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.file,
stickerPackItem: nil
staticEmoji: nil,
subgroupId: nil
)
let groupId = "saved"
@ -292,7 +399,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -308,9 +415,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.media,
stickerPackItem: nil
staticEmoji: nil,
subgroupId: nil
)
let groupId = "recent"
@ -318,7 +425,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
}
count += 1
@ -357,9 +464,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
processedIds.insert(item.media.fileId)
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.media,
stickerPackItem: nil
staticEmoji: nil,
subgroupId: nil
)
let groupId = "premium"
@ -367,7 +474,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -377,16 +484,16 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.file,
stickerPackItem: item
staticEmoji: nil,
subgroupId: nil
)
let groupId = entry.index.collectionId
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: true, items: [resultItem]))
}
}
@ -416,7 +523,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items)
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: false, displayPremiumBadges: group.displayPremiumBadges, items: group.items)
},
itemLayoutType: .detailed
)
@ -599,6 +706,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
},
openGifContextMenu: { [weak self] file, sourceView, sourceRect, gesture, isSaved in
guard let strongSelf = self else {
return
}
strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
}
)
@ -790,4 +903,163 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return (expandedHeight, 0.0)
}
private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
let canSaveGif: Bool
if file.fileId.namespace == Namespaces.Media.CloudFile {
canSaveGif = true
} else {
canSaveGif = false
}
let _ = (self.context.engine.stickers.isGifSaved(id: file.fileId)
|> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
guard let strongSelf = self else {
return
}
var isGifSaved = isGifSaved
if !canSaveGif {
isGifSaved = false
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:])
let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
}, baseNavigationController: nil)
gallery.setHintWillBePresentedInPreviewingContext(true)
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
if isSaved {
let _ = self?.controllerInteraction.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false)
}/* else if let (collection, result) = file.contextResult {
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false)
}*/
})))
/*if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout {
var isScheduledMessages = false
if case .scheduledMessages = interfaceState.subject {
isScheduledMessages = true
}
if !isScheduledMessages {
if case let .peer(peerId) = interfaceState.chatLocation {
if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat {
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
if isSaved {
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, true, false)
} else if let (collection, result) = file.contextResult {
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true)
}
})))
}
if isSaved {
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, true)
})))
}
}
}
}*/
if isSaved || isGifSaved {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.fileId).start()
})))
} else if canSaveGif && !isGifSaved {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let context = strongSelf.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controllerInteraction = strongSelf.controllerInteraction
let _ = (toggleGifSaved(account: context.account, fileReference: FileMediaReference.savedGif(media: file), saved: true)
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
text = presentationData.strings.Premium_MaxSavedGifsFinalText
} else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
}
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
controllerInteraction.navigationController()?.pushViewController(controller)
return true
}
return false
}), nil)
}
})
})))
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil)
})
}
}
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceView: UIView?
let sourceRect: CGRect
let navigationController: NavigationController? = nil
let passthroughTouches: Bool = false
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) {
self.controller = controller
self.sourceView = sourceView
self.sourceRect = sourceRect
}
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceView = self.sourceView
let sourceRect = self.sourceRect
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
if let sourceView = sourceView {
return (sourceView, sourceRect)
} else {
return nil
}
})
}
func animatedIn() {
if let controller = self.controller as? GalleryController {
controller.viewDidAppear(false)
}
}
}

View file

@ -1642,7 +1642,7 @@ final class ChatMediaInputNode: ChatInputNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil)
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -1651,7 +1651,7 @@ final class ChatMediaInputNode: ChatInputNode {
} else {
text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
@ -1795,7 +1795,7 @@ final class ChatMediaInputNode: ChatInputNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil)
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -1804,7 +1804,7 @@ final class ChatMediaInputNode: ChatInputNode {
} else {
text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
@ -2725,7 +2725,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
let sourceRect = self.sourceRect
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode, sourceRect)
return (sourceNode.view, sourceRect)
} else {
return nil
}

View file

@ -278,7 +278,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)

View file

@ -975,7 +975,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0)
var viaBotApply: (TextNodeLayout, () -> TextNode)?
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)?
var needsReplyBackground = false
var replyMarkup: ReplyMarkupMessageAttribute?
@ -1145,7 +1145,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
}
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in
if let strongSelf = self {
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.updateAccessibilityData(accessibilityData)
@ -1308,7 +1308,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
if let (replyInfoSize, replyInfoApply) = replyInfoApply {
let replyInfoNode = replyInfoApply()
let replyInfoNode = replyInfoApply(synchronousLoads)
if strongSelf.replyInfoNode == nil {
strongSelf.replyInfoNode = replyInfoNode
strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode)

View file

@ -301,7 +301,15 @@ class ChatPresentationContext {
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
let animationRenderer: MultiAnimationRenderer
/*if #available(iOS 13.0, *) {
animationRenderer = MultiAnimationMetalRendererImpl()
} else {*/
animationRenderer = MultiAnimationRendererImpl()
//}
self.animationRenderer = animationRenderer
}
}
@ -1035,7 +1043,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode),
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode),
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
@ -1691,7 +1699,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var adminNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil })
var replyInfoOriginY: CGFloat = 0.0
var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil })
var replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _ in nil })
var forwardInfoOriginY: CGFloat = 0.0
var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?) = (CGSize(), { _ in nil })
@ -1809,7 +1817,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
replyInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) })
replyInfoOriginY = headerSize.height
headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
@ -2283,7 +2291,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
contentUpperRightCorner: CGPoint,
forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?),
forwardInfoOriginY: CGFloat,
replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?),
replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?),
replyInfoOriginY: CGFloat,
removedContentNodeIndices: [Int]?,
addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?,
@ -2489,7 +2497,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
}
if let replyInfoNode = replyInfoSizeApply.1() {
if let replyInfoNode = replyInfoSizeApply.1(synchronousLoads) {
strongSelf.replyInfoNode = replyInfoNode
var animateFrame = true
if replyInfoNode.supernode == nil {

View file

@ -393,7 +393,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
var viaBotApply: (TextNodeLayout, () -> TextNode)?
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)?
var updatedReplyBackgroundNode: NavigationBackgroundNode?
var replyMarkup: ReplyMarkupMessageAttribute?
@ -580,7 +580,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
layoutSize.height += 6.0 + reactionButtonsSizeAndApply.0.height
}
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in
if let strongSelf = self {
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
@ -706,7 +706,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
}
if let (replyInfoSize, replyInfoApply) = replyInfoApply {
let replyInfoNode = replyInfoApply()
let replyInfoNode = replyInfoApply(synchronousLoads)
if strongSelf.replyInfoNode == nil {
strongSelf.replyInfoNode = replyInfoNode
strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode)

View file

@ -90,7 +90,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
self.contentNode.addSubnode(self.lineNode)
}
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode) {
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode)
@ -267,7 +267,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing)
return (size, {
return (size, { attemptSynchronous in
let node: ChatMessageReplyInfoNode
if let maybeNode = maybeNode {
node = maybeNode
@ -283,7 +283,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
let titleNode = titleApply()
var textArguments: TextNodeWithEntities.Arguments?
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor, attemptSynchronous: attemptSynchronous)
}
let textNode = textApply(textArguments)
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil

View file

@ -526,7 +526,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0)
var viaBotApply: (TextNodeLayout, () -> TextNode)?
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)?
var replyMarkup: ReplyMarkupMessageAttribute?
var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)
@ -762,7 +762,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in
if let strongSelf = self {
var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration, _) = animation {
@ -892,7 +892,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
if let (replyInfoSize, replyInfoApply) = replyInfoApply {
let replyInfoNode = replyInfoApply()
let replyInfoNode = replyInfoApply(synchronousLoads)
if strongSelf.replyInfoNode == nil {
strongSelf.replyInfoNode = replyInfoNode
strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode)

View file

@ -249,6 +249,10 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
if let updatingMedia = item.attributes.updatingMedia {
messageEntities = updatingMedia.entities?.entities ?? []
}
}
var entities: [MessageTextEntity]?
@ -404,7 +408,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
return (boundingSize, { [weak self] animation, _, _ in
return (boundingSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
if let updatedCachedChatMessageText = updatedCachedChatMessageText {
@ -432,7 +436,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor))
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads))
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {

View file

@ -685,7 +685,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
context: strongSelf.context,
cache: cache,
renderer: renderer,
placeholderColor: theme.list.mediaPlaceholderColor
placeholderColor: theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
}
let _ = textApply(textArguments)

Some files were not shown because too many files have changed in this diff Show more