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