Animated emoji improvements

This commit is contained in:
Ali 2022-07-19 03:38:07 +02:00
parent 2a5b45883d
commit c141531c7b
58 changed files with 3446 additions and 1133 deletions

View File

@ -7849,3 +7849,5 @@ Sorry for the inconvenience.";
"WebApp.CloseConfirmation" = "Changes that you made may not be saved.";
"WebApp.CloseAnyway" = "Close Anyway";
"Emoji.ClearRecent" = "Clear Recent Emoji";

View File

@ -165,6 +165,7 @@ public protocol AnimatedStickerNode: ASDisplayNode {
var autoplay: Bool { get set }
var visibility: Bool { get set }
var overrideVisibility: Bool { get set }
var isPlayingChanged: (Bool) -> Void { get }
@ -222,6 +223,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
}
public var autoplay = false
public var overrideVisibility: Bool = false
public var visibility = false {
didSet {
@ -386,7 +388,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
private func updateIsPlaying() {
if !self.autoplay {
let isPlaying = self.visibility && self.isDisplaying
let isPlaying = self.visibility && (self.isDisplaying || self.overrideVisibility)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
if isPlaying {

View File

@ -56,6 +56,8 @@ public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode
}
}
public var overrideVisibility: Bool = false
public var isPlayingChanged: (Bool) -> Void = { _ in }
private var sourceDisposable: Disposable?

View File

@ -898,9 +898,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let animationComponent = LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_smiletosticker",
colors: colors,
mode: .animateTransitionFromPrevious
),
colors: colors,
size: CGSize(width: 32.0, height: 32.0)
)
let inputNodeSize = self.inputModeView.update(

View File

@ -661,6 +661,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
let animation: LottieAnimationComponent.AnimationItem?
let colors: [String: UIColor]
let progressValue: Double?
switch state {
case let .downloading(progress):
@ -668,13 +669,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
animation = LottieAnimationComponent.AnimationItem(
name: "anim_search_downloading",
colors: [
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
],
mode: .animating(loop: true)
)
colors = [
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
]
progressValue = progress
strongSelf.clearUnseenDownloadsTimer?.invalidate()
@ -684,18 +685,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
animation = LottieAnimationComponent.AnimationItem(
name: "anim_search_downloaded",
colors: [
"Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
],
mode: .animating(loop: false)
)
colors = [
"Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
]
progressValue = 1.0
if strongSelf.clearUnseenDownloadsTimer == nil {
@ -718,6 +719,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.hasDownloads = hasDownloadsValue
animation = nil
colors = [:]
progressValue = nil
strongSelf.clearUnseenDownloadsTimer?.invalidate()
@ -728,6 +730,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let contentComponent = AnyComponent(ZStack<Empty>([
AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieAnimationComponent(
animation: animation,
colors: colors,
size: CGSize(width: 24.0, height: 24.0)
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(ProgressIndicatorComponent(

View File

@ -458,6 +458,25 @@ public struct Transition {
)
}
}
public func animateSublayerScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
completion?(true)
case let .curve(duration, curve):
view.layer.animate(
from: fromValue as NSNumber,
to: toValue as NSNumber,
keyPath: "sublayerTransform.scale",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: additive,
completion: completion
)
}
}
public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {

View File

@ -20,21 +20,21 @@ public final class LottieAnimationComponent: Component {
public var name: String
public var mode: Mode
public var colors: [String: UIColor]
public init(name: String, colors: [String: UIColor], mode: Mode) {
public init(name: String, mode: Mode) {
self.name = name
self.colors = colors
self.mode = mode
}
}
public let animation: AnimationItem
public let colors: [String: UIColor]
public let tag: AnyObject?
public let size: CGSize?
public init(animation: AnimationItem, tag: AnyObject? = nil, size: CGSize?) {
public init(animation: AnimationItem, colors: [String: UIColor], tag: AnyObject? = nil, size: CGSize?) {
self.animation = animation
self.colors = colors
self.tag = tag
self.size = size
}
@ -42,6 +42,7 @@ public final class LottieAnimationComponent: Component {
public func tagged(_ tag: AnyObject?) -> LottieAnimationComponent {
return LottieAnimationComponent(
animation: self.animation,
colors: self.colors,
tag: tag,
size: self.size
)
@ -51,6 +52,9 @@ public final class LottieAnimationComponent: Component {
if lhs.animation != rhs.animation {
return false
}
if lhs.colors != rhs.colors {
return false
}
if lhs.tag !== rhs.tag {
return false
}
@ -117,6 +121,11 @@ public final class LottieAnimationComponent: Component {
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
var updatePlayback = false
var updateColors = false
if let currentComponent = self.component, currentComponent.colors != component.colors {
updateColors = true
}
if self.component?.animation != component.animation {
if let animationView = self.animationView {
@ -158,15 +167,7 @@ public final class LottieAnimationComponent: Component {
view.backgroundColor = .clear
view.isOpaque = false
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"))
}
updateColors = true
self.animationView = view
self.addSubview(view)
@ -176,6 +177,23 @@ public final class LottieAnimationComponent: Component {
}
}
self.component = component
if updateColors, let animationView = self.animationView {
if let value = component.colors["__allcolors__"] {
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
for (key, value) in component.colors {
if key == "__allcolors__" {
continue
}
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
}
var animationSize = CGSize()
if let animationView = self.animationView, let animation = animationView.animation {
animationSize = animation.size
@ -187,7 +205,36 @@ public final class LottieAnimationComponent: Component {
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationView = self.animationView {
animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
if animationView.frame != animationFrame {
if !transition.animation.isImmediate && !animationView.frame.isEmpty && animationView.frame.size != animationFrame.size {
let previouosAnimationFrame = animationView.frame
if let snapshotView = animationView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = previouosAnimationFrame
animationView.superview?.insertSubview(snapshotView, belowSubview: animationView)
transition.setPosition(view: snapshotView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
snapshotView.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
let scaleFactor = previouosAnimationFrame.width / animationFrame.width
transition.animateScale(view: snapshotView, from: scaleFactor, to: 1.0)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
transition.setPosition(view: animationView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
transition.setBounds(view: animationView, bounds: CGRect(origin: CGPoint(), size: animationFrame.size))
transition.animateSublayerScale(view: animationView, from: previouosAnimationFrame.width / animationFrame.width, to: 1.0)
animationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else if animationView.frame.size == animationFrame.size {
transition.setFrame(view: animationView, frame: animationFrame)
} else {
animationView.frame = animationFrame
}
}
if updatePlayback {
if case .animating = component.animation.mode {

View File

@ -261,6 +261,10 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
private var isTopPanelExpanded: Bool = false
public var topPanelComponentView: UIView? {
return self.topPanelView?.componentView
}
override init(frame: CGRect) {
super.init(frame: frame)
@ -774,6 +778,14 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
self.component?.isTopPanelExpandedUpdated(self.isTopPanelExpanded, transition)
}
public func collapseTopPanel() {
if !self.isTopPanelExpanded {
return
}
self.isTopPanelExpandedUpdated(isExpanded: false, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
}
public func makeView() -> View {

View File

@ -68,6 +68,11 @@ public final class PeekController: ViewController, ContextControllerProtocol {
private var animatedIn = false
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?) {
self.presentationData = presentationData
self.content = content

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
public enum PeekControllerContentPresentation {
case contained
@ -26,6 +27,7 @@ public protocol PeekControllerContent {
}
public protocol PeekControllerContentNode {
func ready() -> Signal<Bool, NoError>
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
}

View File

@ -126,6 +126,8 @@ final class PeekControllerNode: ViewControllerTracingNode {
}
self.hapticFeedback.prepareTap()
controller.ready.set(self.contentNode.ready())
}
deinit {

View File

@ -64,6 +64,8 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let _ready = Promise<Bool>()
init(account: Account, item: ImportStickerPack.Sticker) {
self.account = account
self.item = item
@ -104,6 +106,21 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
}
self.addSubnode(self.textNode)
if let animationNode = self.animationNode {
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf._ready.set(.single(true))
}
} else {
self._ready.set(.single(true))
}
}
func ready() -> Signal<Bool, NoError> {
return self._ready.get()
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {

View File

@ -101,6 +101,8 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let _ready = Promise<Bool>()
init(account: Account, item: StickerPreviewPeekItem) {
self.account = account
self.item = item
@ -117,6 +119,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
if item.file.isAnimatedSticker || item.file.isVideoSticker {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.overrideVisibility = true
self.animationNode = animationNode
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
@ -166,12 +169,32 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
if let additionalAnimationNode = self.additionalAnimationNode {
self.addSubnode(additionalAnimationNode)
}
if let animationNode = self.animationNode {
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf._ready.set(.single(true))
}
} else {
self.imageNode.imageUpdated = { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf._ready.set(.single(true))
}
}
}
deinit {
self.effectDisposable.dispose()
}
public func ready() -> Signal<Bool, NoError> {
return self._ready.get()
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let boundingSize: CGSize
if let _ = self.additionalAnimationNode {

View File

@ -544,6 +544,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[982592842] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow($0) }
dict[-732254058] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoUnknown($0) }
dict[-368917890] = { return Api.PaymentCharge.parse_paymentCharge($0) }
dict[-1996951013] = { return Api.PaymentFormMethod.parse_paymentFormMethod($0) }
dict[-1868808300] = { return Api.PaymentRequestedInfo.parse_paymentRequestedInfo($0) }
dict[-842892769] = { return Api.PaymentSavedCredentials.parse_paymentSavedCredentialsCard($0) }
dict[-1566230754] = { return Api.Peer.parse_peerChannel($0) }
@ -710,6 +711,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) }
dict[768691932] = { return Api.StickerSet.parse_stickerSet($0) }
dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) }
dict[451763941] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) }
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
dict[-1609668650] = { return Api.Theme.parse_theme($0) }
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) }
@ -805,6 +807,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1767677564] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) }
dict[-1842450928] = { return Api.Update.parse_updateReadChannelInbox($0) }
dict[-1218471511] = { return Api.Update.parse_updateReadChannelOutbox($0) }
dict[-78886548] = { return Api.Update.parse_updateReadFeaturedEmojiStickers($0) }
dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) }
dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) }
dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) }
@ -1003,7 +1006,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
dict[-1340916937] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1288001087] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
@ -1418,6 +1421,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.PaymentCharge:
_1.serialize(buffer, boxed)
case let _1 as Api.PaymentFormMethod:
_1.serialize(buffer, boxed)
case let _1 as Api.PaymentRequestedInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.PaymentSavedCredentials:

View File

@ -326,6 +326,46 @@ public extension Api {
}
}
public extension Api {
enum PaymentFormMethod: TypeConstructorDescription {
case paymentFormMethod(url: String, title: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .paymentFormMethod(let url, let title):
if boxed {
buffer.appendInt32(-1996951013)
}
serializeString(url, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .paymentFormMethod(let url, let title):
return ("paymentFormMethod", [("url", String(describing: url)), ("title", String(describing: title))])
}
}
public static func parse_paymentFormMethod(_ reader: BufferReader) -> PaymentFormMethod? {
var _1: String?
_1 = parseString(reader)
var _2: String?
_2 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.PaymentFormMethod.paymentFormMethod(url: _1!, title: _2!)
}
else {
return nil
}
}
}
}
public extension Api {
enum PaymentRequestedInfo: TypeConstructorDescription {
case paymentRequestedInfo(flags: Int32, name: String?, phone: String?, email: String?, shippingAddress: Api.PostAddress?)
@ -1228,99 +1268,3 @@ public extension Api {
}
}
public extension Api {
enum Photo: TypeConstructorDescription {
case photo(flags: Int32, id: Int64, accessHash: Int64, fileReference: Buffer, date: Int32, sizes: [Api.PhotoSize], videoSizes: [Api.VideoSize]?, dcId: Int32)
case photoEmpty(id: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId):
if boxed {
buffer.appendInt32(-82216347)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
serializeInt64(accessHash, buffer: buffer, boxed: false)
serializeBytes(fileReference, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(sizes.count))
for item in sizes {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(videoSizes!.count))
for item in videoSizes! {
item.serialize(buffer, true)
}}
serializeInt32(dcId, buffer: buffer, boxed: false)
break
case .photoEmpty(let id):
if boxed {
buffer.appendInt32(590459437)
}
serializeInt64(id, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId):
return ("photo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("sizes", String(describing: sizes)), ("videoSizes", String(describing: videoSizes)), ("dcId", String(describing: dcId))])
case .photoEmpty(let id):
return ("photoEmpty", [("id", String(describing: id))])
}
}
public static func parse_photo(_ reader: BufferReader) -> Photo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int64?
_3 = reader.readInt64()
var _4: Buffer?
_4 = parseBytes(reader)
var _5: Int32?
_5 = reader.readInt32()
var _6: [Api.PhotoSize]?
if let _ = reader.readInt32() {
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self)
}
var _7: [Api.VideoSize]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self)
} }
var _8: Int32?
_8 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.Photo.photo(flags: _1!, id: _2!, accessHash: _3!, fileReference: _4!, date: _5!, sizes: _6!, videoSizes: _7, dcId: _8!)
}
else {
return nil
}
}
public static func parse_photoEmpty(_ reader: BufferReader) -> Photo? {
var _1: Int64?
_1 = reader.readInt64()
let _c1 = _1 != nil
if _c1 {
return Api.Photo.photoEmpty(id: _1!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,99 @@
public extension Api {
enum Photo: TypeConstructorDescription {
case photo(flags: Int32, id: Int64, accessHash: Int64, fileReference: Buffer, date: Int32, sizes: [Api.PhotoSize], videoSizes: [Api.VideoSize]?, dcId: Int32)
case photoEmpty(id: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId):
if boxed {
buffer.appendInt32(-82216347)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
serializeInt64(accessHash, buffer: buffer, boxed: false)
serializeBytes(fileReference, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(sizes.count))
for item in sizes {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(videoSizes!.count))
for item in videoSizes! {
item.serialize(buffer, true)
}}
serializeInt32(dcId, buffer: buffer, boxed: false)
break
case .photoEmpty(let id):
if boxed {
buffer.appendInt32(590459437)
}
serializeInt64(id, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId):
return ("photo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("sizes", String(describing: sizes)), ("videoSizes", String(describing: videoSizes)), ("dcId", String(describing: dcId))])
case .photoEmpty(let id):
return ("photoEmpty", [("id", String(describing: id))])
}
}
public static func parse_photo(_ reader: BufferReader) -> Photo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int64?
_3 = reader.readInt64()
var _4: Buffer?
_4 = parseBytes(reader)
var _5: Int32?
_5 = reader.readInt32()
var _6: [Api.PhotoSize]?
if let _ = reader.readInt32() {
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self)
}
var _7: [Api.VideoSize]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self)
} }
var _8: Int32?
_8 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.Photo.photo(flags: _1!, id: _2!, accessHash: _3!, fileReference: _4!, date: _5!, sizes: _6!, videoSizes: _7, dcId: _8!)
}
else {
return nil
}
}
public static func parse_photoEmpty(_ reader: BufferReader) -> Photo? {
var _1: Int64?
_1 = reader.readInt64()
let _c1 = _1 != nil
if _c1 {
return Api.Photo.photoEmpty(id: _1!)
}
else {
return nil
}
}
}
}
public extension Api {
enum PhotoSize: TypeConstructorDescription {
case photoCachedSize(type: String, w: Int32, h: Int32, bytes: Buffer)
@ -848,87 +944,3 @@ public extension Api {
}
}
public extension Api {
enum ReactionCount: TypeConstructorDescription {
case reactionCount(flags: Int32, reaction: String, count: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .reactionCount(let flags, let reaction, let count):
if boxed {
buffer.appendInt32(1873957073)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(reaction, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .reactionCount(let flags, let reaction, let count):
return ("reactionCount", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("count", String(describing: count))])
}
}
public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: Int32?
_3 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!)
}
else {
return nil
}
}
}
}
public extension Api {
enum ReceivedNotifyMessage: TypeConstructorDescription {
case receivedNotifyMessage(id: Int32, flags: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .receivedNotifyMessage(let id, let flags):
if boxed {
buffer.appendInt32(-1551583367)
}
serializeInt32(id, buffer: buffer, boxed: false)
serializeInt32(flags, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .receivedNotifyMessage(let id, let flags):
return ("receivedNotifyMessage", [("id", String(describing: id)), ("flags", String(describing: flags))])
}
}
public static func parse_receivedNotifyMessage(_ reader: BufferReader) -> ReceivedNotifyMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.ReceivedNotifyMessage.receivedNotifyMessage(id: _1!, flags: _2!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,87 @@
public extension Api {
enum ReactionCount: TypeConstructorDescription {
case reactionCount(flags: Int32, reaction: String, count: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .reactionCount(let flags, let reaction, let count):
if boxed {
buffer.appendInt32(1873957073)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(reaction, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .reactionCount(let flags, let reaction, let count):
return ("reactionCount", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("count", String(describing: count))])
}
}
public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: Int32?
_3 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!)
}
else {
return nil
}
}
}
}
public extension Api {
enum ReceivedNotifyMessage: TypeConstructorDescription {
case receivedNotifyMessage(id: Int32, flags: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .receivedNotifyMessage(let id, let flags):
if boxed {
buffer.appendInt32(-1551583367)
}
serializeInt32(id, buffer: buffer, boxed: false)
serializeInt32(flags, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .receivedNotifyMessage(let id, let flags):
return ("receivedNotifyMessage", [("id", String(describing: id)), ("flags", String(describing: flags))])
}
}
public static func parse_receivedNotifyMessage(_ reader: BufferReader) -> ReceivedNotifyMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.ReceivedNotifyMessage.receivedNotifyMessage(id: _1!, flags: _2!)
}
else {
return nil
}
}
}
}
public extension Api {
enum RecentMeUrl: TypeConstructorDescription {
case recentMeUrlChat(url: String, chatId: Int64)

View File

@ -87,6 +87,7 @@ public extension Api {
public extension Api {
enum StickerSetCovered: TypeConstructorDescription {
case stickerSetCovered(set: Api.StickerSet, cover: Api.Document)
case stickerSetFullCovered(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document])
case stickerSetMultiCovered(set: Api.StickerSet, covers: [Api.Document])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -98,6 +99,22 @@ public extension Api {
set.serialize(buffer, true)
cover.serialize(buffer, true)
break
case .stickerSetFullCovered(let set, let packs, let documents):
if boxed {
buffer.appendInt32(451763941)
}
set.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(packs.count))
for item in packs {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(documents.count))
for item in documents {
item.serialize(buffer, true)
}
break
case .stickerSetMultiCovered(let set, let covers):
if boxed {
buffer.appendInt32(872932635)
@ -116,6 +133,8 @@ public extension Api {
switch self {
case .stickerSetCovered(let set, let cover):
return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))])
case .stickerSetFullCovered(let set, let packs, let documents):
return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("documents", String(describing: documents))])
case .stickerSetMultiCovered(let set, let covers):
return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))])
}
@ -139,6 +158,29 @@ public extension Api {
return nil
}
}
public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? {
var _1: Api.StickerSet?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StickerSet
}
var _2: [Api.StickerPack]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self)
}
var _3: [Api.Document]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, documents: _3!)
}
else {
return nil
}
}
public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? {
var _1: Api.StickerSet?
if let signature = reader.readInt32() {
@ -592,6 +634,7 @@ public extension Api {
case updateReadChannelDiscussionOutbox(channelId: Int64, topMsgId: Int32, readMaxId: Int32)
case updateReadChannelInbox(flags: Int32, folderId: Int32?, channelId: Int64, maxId: Int32, stillUnreadCount: Int32, pts: Int32)
case updateReadChannelOutbox(channelId: Int64, maxId: Int32)
case updateReadFeaturedEmojiStickers
case updateReadFeaturedStickers
case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32)
case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32)
@ -1332,6 +1375,12 @@ public extension Api {
}
serializeInt64(channelId, buffer: buffer, boxed: false)
serializeInt32(maxId, buffer: buffer, boxed: false)
break
case .updateReadFeaturedEmojiStickers:
if boxed {
buffer.appendInt32(-78886548)
}
break
case .updateReadFeaturedStickers:
if boxed {
@ -1660,6 +1709,8 @@ public extension Api {
return ("updateReadChannelInbox", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("channelId", String(describing: channelId)), ("maxId", String(describing: maxId)), ("stillUnreadCount", String(describing: stillUnreadCount)), ("pts", String(describing: pts))])
case .updateReadChannelOutbox(let channelId, let maxId):
return ("updateReadChannelOutbox", [("channelId", String(describing: channelId)), ("maxId", String(describing: maxId))])
case .updateReadFeaturedEmojiStickers:
return ("updateReadFeaturedEmojiStickers", [])
case .updateReadFeaturedStickers:
return ("updateReadFeaturedStickers", [])
case .updateReadHistoryInbox(let flags, let folderId, let peer, let maxId, let stillUnreadCount, let pts, let ptsCount):
@ -3196,6 +3247,9 @@ public extension Api {
return nil
}
}
public static func parse_updateReadFeaturedEmojiStickers(_ reader: BufferReader) -> Update? {
return Api.Update.updateReadFeaturedEmojiStickers
}
public static func parse_updateReadFeaturedStickers(_ reader: BufferReader) -> Update? {
return Api.Update.updateReadFeaturedStickers
}

View File

@ -728,13 +728,13 @@ public extension Api.payments {
}
public extension Api.payments {
enum PaymentForm: TypeConstructorDescription {
case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User])
case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users):
if boxed {
buffer.appendInt32(-1340916937)
buffer.appendInt32(1288001087)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(formId, buffer: buffer, boxed: false)
@ -747,6 +747,11 @@ public extension Api.payments {
serializeString(url, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 4) != 0 {serializeString(nativeProvider!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {nativeParams!.serialize(buffer, true)}
if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(additionalMethods!.count))
for item in additionalMethods! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)}
if Int(flags) & Int(1 << 1) != 0 {savedCredentials!.serialize(buffer, true)}
buffer.appendInt32(481674261)
@ -760,8 +765,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))])
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users):
return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("additionalMethods", String(describing: additionalMethods)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))])
}
}
@ -794,17 +799,21 @@ public extension Api.payments {
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.DataJSON
} }
var _12: Api.PaymentRequestedInfo?
var _12: [Api.PaymentFormMethod]?
if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() {
_12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentFormMethod.self)
} }
var _13: Api.PaymentRequestedInfo?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo
_13 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo
} }
var _13: Api.PaymentSavedCredentials?
var _14: Api.PaymentSavedCredentials?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials
_14 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials
} }
var _14: [Api.User]?
var _15: [Api.User]?
if let _ = reader.readInt32() {
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
@ -817,11 +826,12 @@ public extension Api.payments {
let _c9 = _9 != nil
let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 0) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 1) == 0) || _13 != nil
let _c14 = _14 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, savedInfo: _12, savedCredentials: _13, users: _14!)
let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 0) == 0) || _13 != nil
let _c14 = (Int(_1!) & Int(1 << 1) == 0) || _14 != nil
let _c15 = _15 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 {
return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, additionalMethods: _12, savedInfo: _13, savedCredentials: _14, users: _15!)
}
else {
return nil

View File

@ -4215,6 +4215,21 @@ public extension Api.functions.messages {
})
}
}
public extension Api.functions.messages {
static func getFeaturedEmojiStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.FeaturedStickers>) {
let buffer = Buffer()
buffer.appendInt32(248473398)
serializeInt64(hash, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getFeaturedEmojiStickers", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.FeaturedStickers? in
let reader = BufferReader(buffer)
var result: Api.messages.FeaturedStickers?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.FeaturedStickers
}
return result
})
}
}
public extension Api.functions.messages {
static func getFeaturedStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.FeaturedStickers>) {
let buffer = Buffer()
@ -6295,11 +6310,11 @@ public extension Api.functions.payments {
}
}
public extension Api.functions.payments {
static func canPurchasePremium() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1435856696)
return (FunctionDescription(name: "payments.canPurchasePremium", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
buffer.appendInt32(-1614700874)
purpose.serialize(buffer, true)
return (FunctionDescription(name: "payments.canPurchasePremium", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {

View File

@ -800,13 +800,13 @@ public final class MediaStreamComponent: CombinedComponent {
AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_profilemore",
colors: [
"Point 2.Group 1.Fill 1": whiteColor,
"Point 3.Group 1.Fill 1": whiteColor,
"Point 1.Group 1.Fill 1": whiteColor
],
mode: .still(position: .begin)
),
colors: [
"Point 2.Group 1.Fill 1": whiteColor,
"Point 3.Group 1.Fill 1": whiteColor,
"Point 1.Group 1.Fill 1": whiteColor
],
size: CGSize(width: 22.0, height: 22.0)
).tagged(moreAnimationTag))),
])),

View File

@ -351,6 +351,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
}
var addedHashtags: [String] = []
var emojiItems: [RecentEmojiItem] = []
var localGroupingKeyBySourceKey: [Int64: Int64] = [:]
@ -475,6 +476,13 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
let hashtag = nsText.substring(with: entityRange)
addedHashtags.append(hashtag)
}
} else if case let .CustomEmoji(_, fileId) = entity.type {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if let file = inlineStickers[mediaId] as? TelegramMediaFile {
emojiItems.append(RecentEmojiItem(.file(file)))
} else if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
emojiItems.append(RecentEmojiItem(.file(file)))
}
}
}
break
@ -786,6 +794,19 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
}
var messageIds: [MessageId?] = []
if !storeMessages.isEmpty {
for emojiItem in emojiItems {
if let entry = CodableEntry(emojiItem) {
let id: RecentEmojiItemId
switch emojiItem.content {
case let .file(file):
id = RecentEmojiItemId(file.fileId)
case let .text(text):
id = RecentEmojiItemId(text)
}
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji, item: OrderedItemListEntry(id: id.rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
}
let globallyUniqueIdToMessageId = transaction.addMessages(storeMessages, location: .Random)
for globallyUniqueId in globallyUniqueIds {
messageIds.append(globallyUniqueIdToMessageId[globallyUniqueId])

View File

@ -316,6 +316,7 @@ public final class AccountViewTracker {
private var channelPollingContexts: [PeerId: ChannelPollingContext] = [:]
private var featuredStickerPacksContext: FeaturedStickerPacksContext?
private var featuredEmojiPacksContext: FeaturedStickerPacksContext?
let chatHistoryPreloadManager: ChatHistoryPreloadManager
@ -1711,7 +1712,7 @@ public final class AccountViewTracker {
let timestamp = CFAbsoluteTimeGetCurrent()
if context.timestamp == nil || abs(context.timestamp! - timestamp) > 60.0 * 60.0 {
context.timestamp = timestamp
context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox).start())
context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox, category: .stickerPacks).start())
}
let index = context.subscribers.add(Void())
@ -1736,6 +1737,56 @@ public final class AccountViewTracker {
}
}
public func featuredEmojiPacks() -> Signal<[FeaturedStickerPackItem], NoError> {
return Signal { subscriber in
if let account = self.account {
let view = account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)]).start(next: { next in
if let view = next.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)] as? OrderedItemListView {
subscriber.putNext(view.items.map { $0.contents.get(FeaturedStickerPackItem.self)! })
} else {
subscriber.putNext([])
}
}, completed: {
subscriber.putCompletion()
})
let disposable = MetaDisposable()
self.queue.async {
let context: FeaturedStickerPacksContext
if let current = self.featuredEmojiPacksContext {
context = current
} else {
context = FeaturedStickerPacksContext()
self.featuredEmojiPacksContext = context
}
let timestamp = CFAbsoluteTimeGetCurrent()
if context.timestamp == nil || abs(context.timestamp! - timestamp) > 60.0 * 60.0 {
context.timestamp = timestamp
context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox, category: .emojiPacks).start())
}
let index = context.subscribers.add(Void())
disposable.set(ActionDisposable {
self.queue.async {
if let context = self.featuredEmojiPacksContext {
context.subscribers.remove(index)
}
}
})
}
return ActionDisposable {
view.dispose()
disposable.dispose()
}
} else {
subscriber.putNext([])
subscriber.putCompletion()
return EmptyDisposable
}
}
}
public func callListView(type: CallListViewType, index: MessageIndex, count: Int) -> Signal<CallListView, NoError> {
if let account = self.account {
let granularity: Int32 = 60 * 60 * 24

View File

@ -220,10 +220,12 @@ private func installRemoteStickerPacks(network: Network, infos: [StickerPackColl
var archivedIds = Set<ItemCollectionId>()
for archivedSet in archivedSets {
switch archivedSet {
case let .stickerSetCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetMultiCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetMultiCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetFullCovered(set, _, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
}
}
return archivedIds

View File

@ -3,6 +3,30 @@ import TelegramApi
import Postbox
import SwiftSignalKit
enum FeaturedStickerPacksCategory {
case stickerPacks
case emojiPacks
}
extension FeaturedStickerPacksCategory {
var itemListNamespace: Int32 {
switch self {
case .stickerPacks:
return Namespaces.OrderedItemList.CloudFeaturedStickerPacks
case .emojiPacks:
return Namespaces.OrderedItemList.CloudFeaturedEmojiPacks
}
}
var collectionIdNamespace: Int32 {
switch self {
case .stickerPacks:
return Namespaces.ItemCollection.CloudStickerPacks
case .emojiPacks:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
}
private func hashForIdsReverse(_ ids: [Int64]) -> Int64 {
var acc: UInt64 = 0
@ -24,9 +48,9 @@ func manageStickerPacks(network: Network, postbox: Postbox) -> Signal<Void, NoEr
} |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: FeaturedStickerPacksCategory) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
let initialPacks = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let initialPacks = transaction.getOrderedListItems(collectionId: category.itemListNamespace)
var initialPackMap: [Int64: FeaturedStickerPackItem] = [:]
for entry in initialPacks {
let item = entry.contents.get(FeaturedStickerPackItem.self)!
@ -37,18 +61,29 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal<V
return FeaturedStickerPackItemId($0.id).packId
}
let initialHash: Int64 = hashForIdsReverse(initialPackIds)
return network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash))
|> retryRequest
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
struct FeaturedListContent {
var unreadIds: Set<Int64>
var packs: [FeaturedStickerPackItem]
var isPremium: Bool
}
enum FeaturedList {
case notModified
case content(FeaturedListContent)
}
let signal: Signal<FeaturedList, NoError>
switch category {
case .stickerPacks:
signal = network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash))
|> map { result -> FeaturedList in
switch result {
case .featuredStickersNotModified:
break
return .notModified
case let .featuredStickers(flags, _, _, sets, unread):
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
var (info, items) = parsePreviewStickerSet(set)
var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace)
if let previousPack = initialPackMap[info.id.id] {
if previousPack.info.hash == info.hash {
items = previousPack.topItems
@ -56,7 +91,56 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal<V
}
updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id)))
}
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, items: updatedPacks.compactMap { item -> OrderedItemListEntry? in
let isPremium = flags & (1 << 0) != 0
return .content(FeaturedListContent(
unreadIds: unreadIds,
packs: updatedPacks,
isPremium: isPremium
))
}
}
|> `catch` { _ -> Signal<FeaturedList, NoError> in
return .single(.notModified)
}
case .emojiPacks:
signal = network.request(Api.functions.messages.getFeaturedEmojiStickers(hash: initialHash))
|> map { result -> FeaturedList in
switch result {
case .featuredStickersNotModified:
return .notModified
case let .featuredStickers(flags, _, _, sets, unread):
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace)
if let previousPack = initialPackMap[info.id.id] {
if previousPack.info.hash == info.hash {
items = previousPack.topItems
}
}
updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id)))
}
let isPremium = flags & (1 << 0) != 0
return .content(FeaturedListContent(
unreadIds: unreadIds,
packs: updatedPacks,
isPremium: isPremium
))
}
}
|> `catch` { _ -> Signal<FeaturedList, NoError> in
return .single(.notModified)
}
}
return signal
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
switch result {
case .notModified:
break
case let .content(content):
transaction.replaceOrderedItemListItems(collectionId: category.itemListNamespace, items: content.packs.compactMap { item -> OrderedItemListEntry? in
if let entry = CodableEntry(item) {
return OrderedItemListEntry(id: FeaturedStickerPackItemId(item.info.id.id).rawValue, contents: entry)
} else {
@ -64,14 +148,14 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal<V
}
})
let isPremium = flags & (1 << 0) != 0
if let entry = CodableEntry(FeaturedStickersConfiguration(isPremium: isPremium)) {
if let entry = CodableEntry(FeaturedStickersConfiguration(isPremium: content.isPremium)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, key: ValueBoxKey(length: 0)), entry: entry)
}
}
}
}
} |> switchToLatest
}
|> switchToLatest
}
public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, offset: Int, limit: Int) -> Signal<[FeaturedStickerPackItem], NoError> {
@ -85,7 +169,7 @@ public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, o
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
let (info, items) = parsePreviewStickerSet(set)
let (info, items) = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id)))
}
return updatedPacks
@ -125,23 +209,55 @@ public func preloadedFeaturedStickerSet(network: Network, postbox: Postbox, id:
} |> switchToLatest
}
func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace = Namespaces.ItemCollection.CloudStickerPacks) -> (StickerPackCollectionInfo, [StickerPackItem]) {
func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace) -> (StickerPackCollectionInfo, [StickerPackItem]) {
switch set {
case let .stickerSetCovered(set, cover):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
case let .stickerSetCovered(set, cover):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
if let file = telegramMediaFileFromApiDocument(cover), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []))
}
return (info, items)
case let .stickerSetMultiCovered(set, covers):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
for cover in covers {
if let file = telegramMediaFileFromApiDocument(cover), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []))
}
return (info, items)
case let .stickerSetMultiCovered(set, covers):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
for cover in covers {
if let file = telegramMediaFileFromApiDocument(cover), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []))
}
return (info, items)
case let .stickerSetFullCovered(set, packs, documents):
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
break
}
return (info, items)
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
for document in documents {
if let file = telegramMediaFileFromApiDocument(document), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
return (info, items)
}
}

View File

@ -45,3 +45,6 @@ func _internal_clearRecentlyUsedStickers(transaction: Transaction) {
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .clear)
}
func _internal_clearRecentlyUsedEmoji(transaction: Transaction) {
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji, items: [])
}

View File

@ -64,6 +64,8 @@ public struct Namespaces {
public static let RecentDownloads: Int32 = 11
public static let PremiumStickers: Int32 = 12
public static let CloudPremiumStickers: Int32 = 13
public static let LocalRecentEmoji: Int32 = 14
public static let CloudFeaturedEmojiPacks: Int32 = 15
}
public struct CachedItemCollection {

View File

@ -49,3 +49,106 @@ public final class RecentMediaItem: Codable, Equatable {
return lhs.media.isEqual(to: rhs.media)
}
}
public struct RecentEmojiItemId {
public enum Id {
case media(MediaId)
case text(String)
}
public let rawValue: MemoryBuffer
public let id: Id
public init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
assert(rawValue.length >= 1)
var type: UInt8 = 0
memcpy(&type, rawValue.memory.advanced(by: 0), 1)
if type == 0 {
assert(rawValue.length == 1 + 4 + 8)
var mediaIdNamespace: Int32 = 0
var mediaIdId: Int64 = 0
memcpy(&mediaIdNamespace, rawValue.memory.advanced(by: 1), 4)
memcpy(&mediaIdId, rawValue.memory.advanced(by: 1 + 4), 8)
self.id = .media(MediaId(namespace: mediaIdNamespace, id: mediaIdId))
} else if type == 1 {
var length: UInt16 = 0
assert(rawValue.length >= 1 + 2)
memcpy(&length, rawValue.memory.advanced(by: 1), 2)
assert(rawValue.length >= 1 + 2 + Int(length))
self.id = .text(String(data: Data(bytes: rawValue.memory.advanced(by: 1 + 2), count: Int(length)), encoding: .utf8) ?? ".")
} else {
assert(false)
self.id = .text(".")
}
}
public init(_ mediaId: MediaId) {
self.id = .media(mediaId)
var mediaIdNamespace: Int32 = mediaId.namespace
var mediaIdId: Int64 = mediaId.id
self.rawValue = MemoryBuffer(memory: malloc(1 + 4 + 8)!, capacity: 1 + 4 + 8, length: 1 + 4 + 8, freeWhenDone: true)
var type: UInt8 = 0
memcpy(self.rawValue.memory.advanced(by: 0), &type, 1)
memcpy(self.rawValue.memory.advanced(by: 1), &mediaIdNamespace, 4)
memcpy(self.rawValue.memory.advanced(by: 1 + 4), &mediaIdId, 8)
}
public init(_ text: String) {
self.id = .text(text)
let data = text.data(using: .utf8) ?? Data()
var length: UInt16 = UInt16(data.count)
self.rawValue = MemoryBuffer(memory: malloc(1 + 2 + data.count)!, capacity: 1 + 2 + data.count, length: 1 + 2 + data.count, freeWhenDone: true)
var type: UInt8 = 1
memcpy(self.rawValue.memory.advanced(by: 0), &type, 1)
memcpy(self.rawValue.memory.advanced(by: 1), &length, 2)
data.withUnsafeBytes { bytes in
let _ = memcpy(self.rawValue.memory.advanced(by: 1 + 2), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), bytes.count)
}
}
}
public final class RecentEmojiItem: Codable, Equatable {
public enum Content: Equatable {
case file(TelegramMediaFile)
case text(String)
}
public let content: Content
public init(_ content: Content) {
self.content = content
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
if let mediaData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: "m") {
self.content = .file(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: mediaData.data))))
} else {
self.content = .text(try container.decode(String.self, forKey: "s"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
switch self.content {
case let .file(file):
try container.encode(PostboxEncoder().encodeObjectToRawData(file), forKey: "m")
case let .text(string):
try container.encode(string, forKey: "s")
}
}
public static func ==(lhs: RecentEmojiItem, rhs: RecentEmojiItem) -> Bool {
return lhs.content == rhs.content
}
}

View File

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

View File

@ -213,7 +213,7 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source
|> mapToSignal { result -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> in
return postbox.transaction { transaction -> TelegramMediaInvoice in
switch result {
case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _):
case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _, _):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
var parsedFlags = TelegramMediaInvoiceFlags()
@ -266,10 +266,11 @@ func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, source: B
|> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> in
return postbox.transaction { transaction -> BotPaymentForm in
switch result {
case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers):
case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, additionalMethods, savedInfo, savedCredentials, apiUsers):
let _ = title
let _ = description
let _ = photo
let _ = additionalMethods
var peers: [Peer] = []
for user in apiUsers {

View File

@ -306,7 +306,7 @@ func _internal_searchStickerSetsRemotely(network: Network, query: String) -> Sig
case let .foundStickerSets(_, sets: sets):
var result = FoundStickerSets()
for set in sets {
let parsed = parsePreviewStickerSet(set)
let parsed = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
let values = parsed.1.map({ ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: parsed.0.id, itemIndex: $0.index), item: $0) })
result = result.withUpdatedInfosAndEntries(infos: [(parsed.0.id, parsed.0, parsed.1.first, false)], entries: values)
index += 1

View File

@ -105,7 +105,7 @@ func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaRefe
|> map { result -> [StickerPackReference] in
return result.map { pack in
switch pack {
case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _):
case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _):
let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
return .id(id: info.id.id, accessHash: info.accessHash)
}

View File

@ -140,64 +140,69 @@ public final class CoveredStickerSet : Equatable {
func _internal_installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal<InstallStickerSetResult, InstallStickerSetError> {
return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in
return .generic
} |> mapToSignal { result -> Signal<InstallStickerSetResult, InstallStickerSetError> in
let addResult:InstallStickerSetResult
switch result {
case .stickerSetInstallResultSuccess:
addResult = .successful
case let .stickerSetInstallResultArchive(sets: archived):
var coveredSets:[CoveredStickerSet] = []
for archived in archived {
let apiDocuments:[Api.Document]
let apiSet:Api.StickerSet
switch archived {
case let .stickerSetCovered(set: set, cover: cover):
apiSet = set
apiDocuments = [cover]
case let .stickerSetMultiCovered(set: set, covers: covers):
apiSet = set
apiDocuments = covers
}
|> mapToSignal { result -> Signal<InstallStickerSetResult, InstallStickerSetError> in
let addResult:InstallStickerSetResult
switch result {
case .stickerSetInstallResultSuccess:
addResult = .successful
case let .stickerSetInstallResultArchive(sets: archived):
var coveredSets:[CoveredStickerSet] = []
for archived in archived {
let apiDocuments:[Api.Document]
let apiSet:Api.StickerSet
switch archived {
case let .stickerSetCovered(set: set, cover: cover):
apiSet = set
apiDocuments = [cover]
case let .stickerSetMultiCovered(set: set, covers: covers):
apiSet = set
apiDocuments = covers
case let .stickerSetFullCovered(set, _, documents):
apiSet = set
apiDocuments = documents
}
let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks)
var items:[StickerPackItem] = []
for apiDocument in apiDocuments {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: []))
}
let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks)
var items:[StickerPackItem] = []
for apiDocument in apiDocuments {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: []))
}
coveredSets.append(CoveredStickerSet(info: info, items: items))
}
addResult = .archived(coveredSets)
}
return account.postbox.transaction { transaction -> Void in
var collections = transaction.getCollectionsItems(namespace: info.id.namespace)
var removableIndexes:[Int] = []
for i in 0 ..< collections.count {
if collections[i].0 == info.id {
removableIndexes.append(i)
}
if case let .archived(sets) = addResult {
for set in sets {
if collections[i].0 == set.info.id {
removableIndexes.append(i)
}
}
coveredSets.append(CoveredStickerSet(info: info, items: items))
}
addResult = .archived(coveredSets)
}
for index in removableIndexes.reversed() {
collections.remove(at: index)
}
return account.postbox.transaction { transaction -> Void in
var collections = transaction.getCollectionsItems(namespace: info.id.namespace)
var removableIndexes:[Int] = []
for i in 0 ..< collections.count {
if collections[i].0 == info.id {
removableIndexes.append(i)
}
if case let .archived(sets) = addResult {
for set in sets {
if collections[i].0 == set.info.id {
removableIndexes.append(i)
}
}
}
}
for index in removableIndexes.reversed() {
collections.remove(at: index)
}
collections.insert((info.id, info, items), at: 0)
transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections)
} |> map { _ in return addResult} |> mapError { _ -> InstallStickerSetError in }
collections.insert((info.id, info, items), at: 0)
transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections)
}
|> map { _ in return addResult} |> mapError { _ -> InstallStickerSetError in }
}
}

View File

@ -133,6 +133,13 @@ public extension TelegramEngine {
|> ignoreValues
}
public func clearRecentlyUsedEmoji() -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
_internal_clearRecentlyUsedEmoji(transaction: transaction)
}
|> ignoreValues
}
public func reorderStickerPacks(namespace: ItemCollectionId.Namespace, itemIds: [ItemCollectionId]) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
let infos = transaction.getItemCollectionsInfos(namespace: namespace)

View File

@ -529,7 +529,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear
)
), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)
), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0xffffff, alpha: 0.2), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)
),
freeform: PresentationThemeBubbleColor(
withWallpaper: PresentationThemeBubbleColorComponents(

View File

@ -373,7 +373,7 @@ public struct PresentationResourcesChat {
public static func chatInputMediaPanelGridDismissImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatInputMediaPanelGridDismissImage.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GridDismissIcon"), color: theme.chat.inputMediaPanel.panelIconColor.withAlphaComponent(0.65))
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GridDismissIcon"), color: theme.chat.inputMediaPanel.stickersSectionTextColor)
})
}

View File

@ -110,18 +110,18 @@ public final class AudioTranscriptionButtonComponent: Component {
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: animationName,
colors: [
"icon.Group 3.Stroke 1": foregroundColor,
"icon.Group 1.Stroke 1": foregroundColor,
"icon.Group 4.Stroke 1": foregroundColor,
"icon.Group 2.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
"Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
],
mode: .animateTransitionFromPrevious
),
colors: [
"icon.Group 3.Stroke 1": foregroundColor,
"icon.Group 1.Stroke 1": foregroundColor,
"icon.Group 4.Stroke 1": foregroundColor,
"icon.Group 2.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
"Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
],
size: CGSize(width: 30.0, height: 30.0)
)),
environment: {},
@ -142,11 +142,11 @@ public final class AudioTranscriptionButtonComponent: Component {
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "voicets_progress",
colors: [
"Rectangle 60.Rectangle 60.Stroke 1": foregroundColor
],
mode: .animating(loop: true)
),
colors: [
"Rectangle 60.Rectangle 60.Stroke 1": foregroundColor
],
size: progressFrame.size
)),
environment: {},

View File

@ -142,13 +142,13 @@ public final class AudioTranscriptionPendingLottieIndicatorComponent: Component
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "animated_text_dots",
colors: [
"Comp 1.Point 3.Group 1.Fill 1": component.color,
"Comp 1.Point 2.Group 1.Fill 1": component.color,
"Comp 1.Point 1.Group 1.Fill 1": component.color
],
mode: .animating(loop: true)
),
colors: [
"Comp 1.Point 3.Group 1.Fill 1": component.color,
"Comp 1.Point 2.Group 1.Fill 1": component.color,
"Comp 1.Point 1.Group 1.Fill 1": component.color
],
size: animationSize
)),
environment: {},

View File

@ -13,11 +13,11 @@ import SwiftSignalKit
public final class EntityKeyboardChildEnvironment: Equatable {
public let theme: PresentationTheme
public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>?
public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>?
public init(
theme: PresentationTheme,
getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>?
getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>?
) {
self.theme = theme
self.getContentActiveItemUpdated = getContentActiveItemUpdated
@ -200,8 +200,8 @@ public final class EntityKeyboardComponent: Component {
var contentAccessoryLeftButtons: [AnyComponentWithIdentity<Empty>] = []
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
if transition.userData(MarkInputCollapsed.self) != nil {
self.searchComponent = nil
@ -242,6 +242,8 @@ public final class EntityKeyboardComponent: Component {
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context,
file: emoji.file,
isFeatured: false,
isPremiumLocked: false,
animationCache: component.emojiContent.animationCache,
animationRenderer: component.emojiContent.animationRenderer,
theme: component.theme,
@ -293,8 +295,9 @@ public final class EntityKeyboardComponent: Component {
let iconMapping: [String: String] = [
"saved": "Chat/Input/Media/SavedStickersTabIcon",
"recent": "Chat/Input/Media/RecentTabIcon",
"premium": "Chat/Input/Media/PremiumIcon"
"premium": "Peer Info/PremiumIcon"
]
//TODO:localize
let titleMapping: [String: String] = [
"saved": "Saved",
"recent": "Recent",
@ -323,6 +326,8 @@ public final class EntityKeyboardComponent: Component {
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: stickerContent.context,
file: file,
isFeatured: itemGroup.isFeatured,
isPremiumLocked: itemGroup.isPremiumLocked,
animationCache: stickerContent.animationCache,
animationRenderer: stickerContent.animationRenderer,
theme: component.theme,
@ -375,18 +380,42 @@ public final class EntityKeyboardComponent: Component {
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent)))
var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in component.emojiContent.itemGroups {
if !itemGroup.items.isEmpty {
if let id = itemGroup.groupId.base as? String {
if id == "static" {
if id == "recent" {
let iconMapping: [String: String] = [
"recent": "Chat/Input/Media/RecentTabIcon",
]
//TODO:localize
let titleMapping: [String: String] = [
"recent": "Recent",
]
if let iconName = iconMapping[id], let title = titleMapping[id] {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: iconName,
theme: component.theme,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
} else if id == "static" {
//TODO:localize
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardStaticStickersPanelComponent(
theme: component.theme,
title: "Emoji",
pressed: { [weak self] subgroupId in
guard let strongSelf = self else {
return
@ -404,6 +433,8 @@ public final class EntityKeyboardComponent: Component {
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context,
file: file,
isFeatured: itemGroup.isFeatured,
isPremiumLocked: itemGroup.isPremiumLocked,
animationCache: component.emojiContent.animationCache,
animationRenderer: component.emojiContent.animationRenderer,
theme: component.theme,
@ -651,9 +682,17 @@ public final class EntityKeyboardComponent: Component {
}
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(id: groupId, subgroupId: subgroupId)
guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View else {
return
}
guard let pagerContentView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View else {
return
}
if let topPanelView = pagerView.topPanelComponentView as? EntityKeyboardTopContainerPanelComponent.View {
topPanelView.internalUpdatePanelsAreCollapsed()
}
pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId)
pagerView.collapseTopPanel()
}
private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) {

View File

@ -91,8 +91,6 @@ 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
@ -142,10 +140,6 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
self.addSubview(panelView.view)
}
if !isExpanded {
panelView.isExpanded = false
}
let panelId = panel.id
let _ = panelView.view.update(
transition: panelTransition,
@ -261,6 +255,12 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
self.panelEnvironment?.isExpandedUpdated(hasExpanded, transition)
}
public func internalUpdatePanelsAreCollapsed() {
for (_, panelView) in self.panelViews {
panelView.isExpanded = false
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero {
return nil

View File

@ -18,6 +18,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
let context: AccountContext
let file: TelegramMediaFile
let isFeatured: Bool
let isPremiumLocked: Bool
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
let theme: PresentationTheme
@ -27,6 +29,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
init(
context: AccountContext,
file: TelegramMediaFile,
isFeatured: Bool,
isPremiumLocked: Bool,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
theme: PresentationTheme,
@ -35,6 +39,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
) {
self.context = context
self.file = file
self.isFeatured = isFeatured
self.isPremiumLocked = isPremiumLocked
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.theme = theme
@ -49,6 +55,12 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
if lhs.file.fileId != rhs.file.fileId {
return false
}
if lhs.isFeatured != rhs.isFeatured {
return false
}
if lhs.isPremiumLocked != rhs.isPremiumLocked {
return false
}
if lhs.animationCache !== rhs.animationCache {
return false
}
@ -107,7 +119,6 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
renderer: component.animationRenderer,
placeholderColor: .lightGray,
blurredBadgeColor: .clear,
displayPremiumBadgeIfAvailable: false,
pointSize: CGSize(width: 44.0, height: 44.0),
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else {
@ -130,6 +141,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
if let itemLayer = self.itemLayer {
transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY))
transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
if component.isPremiumLocked {
badge = .locked
} else if component.isFeatured {
badge = .featured
}
itemLayer.update(transition: transition, size: iconFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: component.theme.list.plainBackgroundColor)
itemLayer.isVisibleForAnimations = true
}
@ -144,7 +164,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
let titleSize = titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor))
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)),
insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
)),
environment: {},
containerSize: CGSize(width: 62.0, height: 100.0)
@ -154,7 +175,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
view.alpha = 0.0
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize)
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize)
transition.setAlpha(view: view, alpha: 1.0)
}
} else if let titleView = self.titleView {
@ -274,12 +295,35 @@ final class EntityKeyboardIconTopPanelComponent: Component {
if self.component?.imageName != component.imageName {
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: component.imageName), color: component.theme.chat.inputMediaPanel.panelIconColor)
if component.imageName.hasSuffix("PremiumIcon") {
self.iconView.image = generateImage(CGSize(width: 44.0, height: 42.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = UIImage(bundleImageName: "Peer Info/PremiumIcon") {
if let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
let colorsArray: [CGColor] = [
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x976FFF).cgColor,
UIColor(rgb: 0xE46ACE).cgColor,
UIColor(rgb: 0xE46ACE).cgColor
]
var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
}
})
}
}
self.component = component
let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0)
let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0)
let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 28.0, height: 28.0)
let iconSize = (self.iconView.image?.size ?? nativeIconSize).aspectFitted(boundingIconSize)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize)
@ -297,7 +341,8 @@ final class EntityKeyboardIconTopPanelComponent: Component {
let titleSize = titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor))
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)),
insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
)),
environment: {},
containerSize: CGSize(width: 62.0, height: 100.0)
@ -307,7 +352,7 @@ final class EntityKeyboardIconTopPanelComponent: Component {
view.alpha = 0.0
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize)
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize)
transition.setAlpha(view: view, alpha: 1.0)
}
} else if let titleView = self.titleView {
@ -336,13 +381,16 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
let theme: PresentationTheme
let title: String
let pressed: (EmojiPagerContentComponent.StaticEmojiSegment) -> Void
init(
theme: PresentationTheme,
title: String,
pressed: @escaping (EmojiPagerContentComponent.StaticEmojiSegment) -> Void
) {
self.theme = theme
self.title = title
self.pressed = pressed
}
@ -350,19 +398,29 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let scrollViewContainer: UIView
private let scrollView: UIScrollView
private var visibleItemViews: [EmojiPagerContentComponent.StaticEmojiSegment: ComponentView<Empty>] = [:]
private var titleView: ComponentView<Empty>?
private var component: EntityKeyboardStaticStickersPanelComponent?
private var itemEnvironment: EntityKeyboardTopPanelItemEnvironment?
private var ignoreScrolling: Bool = false
override init(frame: CGRect) {
self.scrollViewContainer = UIView()
self.scrollViewContainer.clipsToBounds = true
self.scrollView = UIScrollView()
super.init(frame: frame)
@ -380,9 +438,9 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.clipsToBounds = true
self.scrollViewContainer.addSubview(self.scrollView)
self.addSubview(self.scrollViewContainer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@ -411,31 +469,36 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
}
private func updateVisibleItems(transition: Transition, animateAppearingItems: Bool) {
guard let component = self.component else {
guard let component = self.component, let itemEnvironment = self.itemEnvironment else {
return
}
let _ = component
var validItemIds = Set<EmojiPagerContentComponent.StaticEmojiSegment>()
let visibleBounds = self.scrollView.bounds
let componentHeight: CGFloat = 32.0
let componentHeight: CGFloat = self.scrollView.contentSize.height
let isExpanded = componentHeight > 32.0
let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases
let itemSize: CGFloat = 28.0
let itemSize: CGFloat = isExpanded ? 42.0 : 32.0
let itemSpacing: CGFloat = 4.0
let sideInset: CGFloat = 2.0
let sideInset: CGFloat = isExpanded ? 5.0 : 2.0
let itemOffset: CGFloat = isExpanded ? -8.0 : 0.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))
let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: floor(componentHeight - itemSize) / 2.0 + itemOffset), size: CGSize(width: itemSize, height: itemSize))
if visibleBounds.intersects(itemFrame) {
let item = items[i]
validItemIds.insert(item)
var animateItem = false
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.visibleItemViews[item] {
itemView = current
} else {
animateItem = animateAppearingItems
itemTransition = .immediate
itemView = ComponentView<Empty>()
self.visibleItemViews[item] = itemView
}
@ -460,14 +523,21 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
animationName = "emojicat_flags"
}
let color: UIColor
if itemEnvironment.highlightedSubgroupId == AnyHashable(items[i].rawValue) {
color = component.theme.chat.inputMediaPanel.panelIconColor.mixedWith(component.theme.chat.inputPanel.primaryTextColor, alpha: 0.35)
} else {
color = component.theme.chat.inputMediaPanel.panelIconColor
}
let _ = itemView.update(
transition: .immediate,
transition: itemTransition,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: animationName,
colors: ["__allcolors__": component.theme.chat.inputMediaPanel.panelIconColor],
mode: animateAppearingItems ? .animating(loop: false) : .still(position: .end)
mode: animateItem ? .animating(loop: false) : .still(position: .end)
),
colors: ["__allcolors__": color],
size: CGSize(width: itemSize, height: itemSize)
)),
environment: {},
@ -477,7 +547,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
if view.superview == nil {
self.scrollView.addSubview(view)
}
view.frame = itemFrame
itemTransition.setFrame(view: view, frame: itemFrame)
}
}
}
@ -495,26 +565,80 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
}
func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.layer.cornerRadius = availableSize.height / 2.0
transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setCornerRadius(layer: self.scrollViewContainer.layer, cornerRadius: min(availableSize.width / 2.0, availableSize.height / 2.0))
let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value
self.component = component
var scrollToItem: AnyHashable?
if itemEnvironment.highlightedSubgroupId != self.itemEnvironment?.highlightedSubgroupId {
scrollToItem = itemEnvironment.highlightedSubgroupId
}
let itemSize: CGFloat = 28.0
self.component = component
self.itemEnvironment = itemEnvironment
let isExpanded = itemEnvironment.isExpanded
let itemSize: CGFloat = isExpanded ? 42.0 : 32.0
let itemSpacing: CGFloat = 4.0
let sideInset: CGFloat = 2.0
let sideInset: CGFloat = isExpanded ? 5.0 : 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.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(availableSize.width, 160.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)
self.updateVisibleItems(transition: transition, animateAppearingItems: false)
if !itemEnvironment.isHighlighted && self.scrollView.contentOffset.x != 0.0 {
if (!itemEnvironment.isHighlighted || isExpanded) && self.scrollView.contentOffset.x != 0.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
scrollToItem = nil
}
if let scrollToItem = scrollToItem {
let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases
for i in 0 ..< items.count {
if AnyHashable(items[i].rawValue) == scrollToItem {
let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: 0.0), size: CGSize(width: itemSize, height: itemSize))
self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -sideInset, dy: 0.0), animated: true)
break
}
}
}
if itemEnvironment.isExpanded {
let titleView: ComponentView<Empty>
if let current = self.titleView {
titleView = current
} else {
titleView = ComponentView<Empty>()
self.titleView = titleView
}
let titleSize = titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)),
insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
)),
environment: {},
containerSize: CGSize(width: 62.0, height: 100.0)
)
if let view = titleView.view {
if view.superview == nil {
view.alpha = 0.0
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 4.0), size: titleSize)
transition.setAlpha(view: view, alpha: 1.0)
}
} else if let titleView = self.titleView {
self.titleView = nil
if let view = titleView.view {
transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
}
return availableSize
@ -533,10 +657,12 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
final class EntityKeyboardTopPanelItemEnvironment: Equatable {
let isExpanded: Bool
let isHighlighted: Bool
let highlightedSubgroupId: AnyHashable?
init(isExpanded: Bool, isHighlighted: Bool) {
init(isExpanded: Bool, isHighlighted: Bool, highlightedSubgroupId: AnyHashable?) {
self.isExpanded = isExpanded
self.isHighlighted = isHighlighted
self.highlightedSubgroupId = highlightedSubgroupId
}
static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool {
@ -546,6 +672,9 @@ final class EntityKeyboardTopPanelItemEnvironment: Equatable {
if lhs.isHighlighted != rhs.isHighlighted {
return false
}
if lhs.highlightedSubgroupId != rhs.highlightedSubgroupId {
return false
}
return true
}
}
@ -617,6 +746,8 @@ private final class ReorderGestureRecognizer: UIGestureRecognizer {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
}
private func longTapTimerFired() {
@ -776,14 +907,14 @@ final class EntityKeyboardTopPanelComponent: Component {
let theme: PresentationTheme
let items: [Item]
let defaultActiveItemId: AnyHashable?
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>
let reorderItems: ([Item]) -> Void
init(
theme: PresentationTheme,
items: [Item],
defaultActiveItemId: AnyHashable? = nil,
activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>,
activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>,
reorderItems: @escaping ([Item]) -> Void
) {
self.theme = theme
@ -836,7 +967,7 @@ final class EntityKeyboardTopPanelComponent: Component {
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.staticExpandedItemSize = self.isExpanded ? self.staticItemSize : CGSize(width: 160.0, height: 32.0)
self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0)
var contentSize = CGSize(width: sideInset, height: height)
@ -887,11 +1018,19 @@ final class EntityKeyboardTopPanelComponent: Component {
return self.items[index].frame
}
func contentFrame(containerFrame: CGRect) -> CGRect {
func contentFrame(index: Int, containerFrame: CGRect) -> CGRect {
let outerFrame = self.items[index].frame
let innerFrame = self.items[index].innerFrame
let sizeDifference = CGSize(width: outerFrame.width - innerFrame.width, height: outerFrame.height - innerFrame.height)
let offsetDifference = CGPoint(x: outerFrame.minX - innerFrame.minX, y: outerFrame.minY - innerFrame.minY)
var frame = containerFrame
frame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0
frame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0
frame.size = self.innerItemSize
frame.origin.x -= offsetDifference.x
frame.origin.y -= offsetDifference.y
frame.size.width -= sizeDifference.width
frame.size.height -= sizeDifference.height
return frame
}
@ -933,12 +1072,17 @@ final class EntityKeyboardTopPanelComponent: Component {
private var isReordering: Bool = false
private var isDraggingOrReordering: Bool = false
private var draggingStoppedTimer: SwiftSignalKit.Timer?
private var draggingFocusItemIndex: Int?
private var draggingEndOffset: CGFloat?
private var isExpanded: Bool = false
private var visibilityFraction: CGFloat = 1.0
private var activeContentItemId: AnyHashable?
private var activeSubcontentItemId: AnyHashable?
private var reorderGestureRecognizer: ReorderGestureRecognizer?
private var component: EntityKeyboardTopPanelComponent?
weak var state: EmptyComponentState?
@ -980,7 +1124,7 @@ final class EntityKeyboardTopPanelComponent: Component {
return strongSelf.scrollView.contentOffset.x > 0.0
}
self.addGestureRecognizer(ReorderGestureRecognizer(
let reorderGestureRecognizer = ReorderGestureRecognizer(
shouldBegin: { [weak self] point in
guard let strongSelf = self else {
return (false, false, nil)
@ -1022,7 +1166,9 @@ final class EntityKeyboardTopPanelComponent: Component {
}
strongSelf.updateIsReordering(isActive)
}
))
)
self.reorderGestureRecognizer = reorderGestureRecognizer
self.addGestureRecognizer(reorderGestureRecognizer)
}
required init?(coder: NSCoder) {
@ -1038,10 +1184,60 @@ final class EntityKeyboardTopPanelComponent: Component {
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.draggingEndOffset = nil
if let component = self.component {
var focusItemIndex: Int?
var location = self.scrollView.panGestureRecognizer.location(in: self.scrollView)
let translation = self.scrollView.panGestureRecognizer.translation(in: self.scrollView)
location.x -= translation.x
location.y -= translation.y
for (id, itemView) in self.itemViews {
if itemView.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
inner: for i in 0 ..< component.items.count {
if id == component.items[i].id {
focusItemIndex = i
break inner
}
}
break
}
}
self.draggingFocusItemIndex = focusItemIndex
}
self.updateIsDragging(true)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
self.draggingEndOffset = scrollView.contentOffset.x
if let component = self.component {
var focusItemIndex: Int?
var location = self.scrollView.panGestureRecognizer.location(in: self.scrollView)
let translation = self.scrollView.panGestureRecognizer.translation(in: self.scrollView)
location.x -= translation.x
location.y -= translation.y
for (id, itemView) in self.itemViews {
if itemView.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
inner: for i in 0 ..< component.items.count {
if id == component.items[i].id {
focusItemIndex = i
break inner
}
}
break
}
}
self.draggingFocusItemIndex = focusItemIndex
}
if !decelerate {
self.updateIsDragging(false)
}
@ -1187,7 +1383,8 @@ final class EntityKeyboardTopPanelComponent: Component {
}
var visibleBounds = self.scrollView.bounds
visibleBounds.size.width += 200.0
visibleBounds.origin.x -= 200.0
visibleBounds.size.width += 400.0
var validIds = Set<AnyHashable>()
let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds)
@ -1212,7 +1409,7 @@ final class EntityKeyboardTopPanelComponent: Component {
transition: itemTransition,
component: item.content,
environment: {
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id)
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id, highlightedSubgroupId: self.activeContentItemId == item.id ? self.activeSubcontentItemId : nil)
},
containerSize: itemOuterFrame.size
)
@ -1259,6 +1456,7 @@ final class EntityKeyboardTopPanelComponent: Component {
}
if self.isReordering {
self.isReordering = false
self.reorderGestureRecognizer?.state = .failed
}
if self.isDraggingOrReordering {
self.isDraggingOrReordering = false
@ -1302,35 +1500,124 @@ final class EntityKeyboardTopPanelComponent: Component {
var updatedBounds: CGRect?
if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout {
if !isExpanded {
if let draggingEndOffset = self.draggingEndOffset {
if abs(self.scrollView.contentOffset.x - draggingEndOffset) > 16.0 {
self.draggingFocusItemIndex = nil
}
} else {
self.draggingFocusItemIndex = nil
}
}
var visibleBounds = self.scrollView.bounds
visibleBounds.size.width += 200.0
visibleBounds.origin.x -= 200.0
visibleBounds.size.width += 400.0
let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds)
if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex {
let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex)
let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex)
var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2)
if !isExpanded {
if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width {
itemIndex = component.items.count - 1
}
if self.scrollView.bounds.minX <= 0.0 {
itemIndex = 0
}
}
var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
var updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)
let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX)
var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize)
var useRightAnchor = false
if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width {
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
itemIndex = component.items.count - 1
useRightAnchor = true
}
if newBounds.minX < 0.0 {
newBounds.origin.x = 0.0
itemIndex = 0
useRightAnchor = false
}
if useRightAnchor {
var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousDistanceToItemRight, y: 0.0), size: availableSize)
if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width {
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
}
if newBounds.minX < 0.0 {
}
}
previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
self.draggingFocusItemIndex = itemIndex
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)// / previousItemFrame.width
let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem/* * updatedItemFrame.width)*/, y: 0.0), size: availableSize)
updatedBounds = newBounds
var updatedVisibleBounds = newBounds
updatedVisibleBounds.size.width += 200.0
updatedVisibleBounds.origin.x -= 200.0
updatedVisibleBounds.size.width += 400.0
let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds)
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[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)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if useRightAnchor {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
}
}
}
} else {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
var itemContainerOriginX = baseFrame.minX
if indexDifference > 0 {
for i in 0 ..< indexDifference {
itemContainerOriginX += previousItemLayout.itemSpacing
itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width
}
} else if indexDifference < 0 {
for i in 0 ..< (-indexDifference) {
itemContainerOriginX -= previousItemLayout.itemSpacing
itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width
}
}
let previousContainerFrame = previousItemLayout.containerFrame(at: index)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
}
}
}
}
}
if !isExpanded {
self.draggingFocusItemIndex = nil
}
}
if self.scrollView.contentSize != itemLayout.contentSize {
@ -1355,7 +1642,13 @@ final class EntityKeyboardTopPanelComponent: Component {
highlightTransition = .immediate
}
highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: activeContentItemId.base is String ? min(itemFrame.width / 2.0, itemFrame.height / 2.0) : 10.0)
let isRound: Bool
if let string = activeContentItemId.base as? String, (string == "recent" || string == "static") {
isRound = true
} else {
isRound = false
}
highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: isRound ? 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 {
@ -1373,11 +1666,11 @@ final class EntityKeyboardTopPanelComponent: Component {
strongSelf.visibilityFractionUpdated(value: fraction, transition: transition)
}
component.activeContentItemIdUpdated.connect { [weak self] (itemId, transition) in
component.activeContentItemIdUpdated.connect { [weak self] (itemId, subcontentItemId, transition) in
guard let strongSelf = self else {
return
}
strongSelf.activeContentItemIdUpdated(itemId: itemId, transition: transition)
strongSelf.activeContentItemIdUpdated(itemId: itemId, subcontentItemId: subcontentItemId, transition: transition)
}
return CGSize(width: availableSize.width, height: height)
@ -1401,14 +1694,15 @@ final class EntityKeyboardTopPanelComponent: Component {
}
}
private func activeContentItemIdUpdated(itemId: AnyHashable, transition: Transition) {
private func activeContentItemIdUpdated(itemId: AnyHashable, subcontentItemId: AnyHashable?, transition: Transition) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
if self.activeContentItemId == itemId {
if self.activeContentItemId == itemId && self.activeSubcontentItemId == subcontentItemId {
return
}
self.activeContentItemId = itemId
self.activeSubcontentItemId = subcontentItemId
let _ = component
let _ = itemLayout

View File

@ -42,12 +42,14 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
if self.shouldBeAnimating {
self.playbackTimer?.invalidate()
let startTimestamp = self.playbackTimestamp + CFAbsoluteTimeGetCurrent()
self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.frameManager?.tick(timestamp: strongSelf.playbackTimestamp)
strongSelf.playbackTimestamp += 1.0 / 30.0
let timestamp = CFAbsoluteTimeGetCurrent() - startTimestamp
strongSelf.frameManager?.tick(timestamp: timestamp)
strongSelf.playbackTimestamp = timestamp
}, queue: .mainQueue())
self.playbackTimer?.start()
} else {
@ -560,7 +562,7 @@ public final class GifPagerContentComponent: Component {
}
private func updateScrollingOffset(transition: Transition) {
let isInteracting = scrollView.isDragging || scrollView.isTracking || scrollView.isDecelerating
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset {
let currentBounds = scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)

View File

@ -14,14 +14,27 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: An
return
}
let frameDuration = 1.0 / Double(animation.frameRate)
for i in 0 ..< animation.frameCount {
let frameSkip: Int
if animation.frameRate >= 60 {
if ProcessInfo.processInfo.activeProcessorCount > 2 {
frameSkip = 1
} else {
frameSkip = 2
}
} else {
frameSkip = 1
}
let frameDuration = Double(frameSkip) / Double(animation.frameRate)
for i in stride(from: 0, through: animation.frameCount - 1, by: frameSkip) {
if writer.isCancelled {
break
}
writer.add(with: { surface in
animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow))
}, proposedWidth: width, proposedHeight: height, duration: frameDuration)
return frameDuration
}, proposedWidth: width, proposedHeight: height)
}
writer.finish()
@ -39,7 +52,8 @@ public func cacheStillSticker(path: String, width: Int, height: Int, writer: Ani
UIGraphicsPopContext()
}
memcpy(surface.argb, context.bytes, surface.height * surface.bytesPerRow)
}, proposedWidth: width, proposedHeight: height, duration: 1.0)
return 1.0
}, proposedWidth: width, proposedHeight: height)
}
writer.finish()

View File

@ -249,14 +249,18 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
}
private final class Frame {
let timestamp: Double
let duration: Double
let textureY: TextureStorage
let textureU: TextureStorage
let textureV: TextureStorage
let textureA: TextureStorage
init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, timestamp: Double) {
self.timestamp = timestamp
var remainingDuration: Double
init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, duration: Double) {
self.duration = duration
self.remainingDuration = duration
self.textureY = textureY
self.textureU = textureU
self.textureV = textureV
@ -281,7 +285,6 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
private let stateUpdated: () -> Void
private var disposable: Disposable?
private var timestamp: Double = 0.0
private var item: AnimationCacheItem?
private(set) var currentFrame: Frame?
@ -354,18 +357,35 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
return self.update(device: device, texturePoolFullPlane: texturePoolFullPlane, texturePoolHalfPlane: texturePoolHalfPlane, advanceTimestamp: advanceTimestamp)
}
private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double?) -> LoadFrameTask? {
private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: 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, !self.isLoadingFrame {
currentFrame.remainingDuration -= advanceTimestamp
}
if let currentFrame = self.currentFrame, currentFrame.timestamp == self.timestamp {
} else if !self.isLoadingFrame {
var frameAdvance: AnimationCacheItem.Advance?
if !self.isLoadingFrame {
if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 {
let divisionFactor = advanceTimestamp / currentFrame.remainingDuration
let wholeFactor = round(divisionFactor)
if abs(wholeFactor - divisionFactor) < 0.005 {
currentFrame.remainingDuration = 0.0
frameAdvance = .frames(Int(wholeFactor))
} else {
currentFrame.remainingDuration -= advanceTimestamp
if currentFrame.remainingDuration <= 0.0 {
frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration))
}
}
} else if self.currentFrame == nil {
frameAdvance = .frames(1)
}
}
if let frameAdvance = frameAdvance, !self.isLoadingFrame {
self.isLoadingFrame = true
let fullParameters = texturePoolFullPlane.parameters
@ -378,7 +398,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
let preferredRowAlignment = self.preferredRowAlignment
return LoadFrameTask(task: { [weak self] in
let frame = item.getFrame(at: timestamp, requestedFormat: .yuva(rowAlignment: preferredRowAlignment))
let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment))
let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane)
let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane)
@ -387,7 +407,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
var currentFrame: Frame?
if let frame = frame, let textureY = textureY, let textureU = textureU, let textureV = textureV, let textureA = textureA {
currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, timestamp: timestamp)
currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, duration: frame.duration)
}
return {

View File

@ -29,6 +29,17 @@ open class MultiAnimationRenderTarget: SimpleLayer {
}
}
public var blurredRepresentationBackgroundColor: UIColor?
public var blurredRepresentationTarget: CALayer? {
didSet {
if self.blurredRepresentationTarget !== oldValue {
for f in self.updateStateCallbacks.copyItems() {
f()
}
}
}
}
public override init() {
assert(Thread.isMainThread)
@ -65,60 +76,6 @@ open class MultiAnimationRenderTarget: SimpleLayer {
}
}
private final class FrameGroup {
let image: UIImage
let badgeImage: UIImage?
let size: CGSize
let timestamp: Double
init?(item: AnimationCacheItem, timestamp: Double) {
guard let firstFrame = item.getFrame(at: timestamp, requestedFormat: .rgba) else {
return nil
}
switch firstFrame.format {
case let .rgba(data, width, height, bytesPerRow):
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
/*var sourceBuffer = vImage_Buffer()
sourceBuffer.width = UInt(width)
sourceBuffer.height = UInt(height)
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound))
sourceBuffer.rowBytes = bytesPerRow
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(32)
destinationBuffer.height = UInt(32)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = bytesPerRow
vImageBoxConvolve_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
UInt(width - 32 - 16), UInt(height - 32 - 16),
UInt32(31),
UInt32(31),
nil,
vImage_Flags(kvImageEdgeExtend))*/
}
guard let image = context.generateImage() else {
return nil
}
self.image = image
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
self.timestamp = timestamp
self.badgeImage = nil
default:
return nil
}
}
}
private final class LoadFrameGroupTask {
let task: () -> () -> Void
@ -128,6 +85,190 @@ private final class LoadFrameGroupTask {
}
private final class ItemAnimationContext {
fileprivate final class Frame {
let frame: AnimationCacheItemFrame
let duration: Double
let image: UIImage
let badgeImage: UIImage?
let size: CGSize
var remainingDuration: Double
private var blurredRepresentationValue: UIImage?
init?(frame: AnimationCacheItemFrame) {
self.frame = frame
self.duration = frame.duration
self.remainingDuration = frame.duration
switch frame.format {
case let .rgba(data, width, height, bytesPerRow):
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
/*var sourceBuffer = vImage_Buffer()
sourceBuffer.width = UInt(width)
sourceBuffer.height = UInt(height)
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound))
sourceBuffer.rowBytes = bytesPerRow
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(32)
destinationBuffer.height = UInt(32)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = bytesPerRow
vImageBoxConvolve_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
UInt(width - 32 - 16), UInt(height - 32 - 16),
UInt32(31),
UInt32(31),
nil,
vImage_Flags(kvImageEdgeExtend))*/
}
guard let image = context.generateImage() else {
return nil
}
self.image = image
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
self.badgeImage = nil
default:
return nil
}
}
func blurredRepresentation(color: UIColor?) -> UIImage? {
if let blurredRepresentationValue = self.blurredRepresentationValue {
return blurredRepresentationValue
}
switch frame.format {
case let .rgba(data, width, height, bytesPerRow):
let blurredWidth = 12
let blurredHeight = 12
let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow)
data.withUnsafeBytes { bytes -> Void in
if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) {
let image = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 32,
bytesPerRow: bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
)
if let image = image {
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
context.withFlippedContext { c in
c.setFillColor((color ?? .white).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
/*var sourceBuffer = vImage_Buffer()
sourceBuffer.width = UInt(width)
sourceBuffer.height = UInt(height)
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!)
sourceBuffer.rowBytes = bytesPerRow
let tempBufferBytes = malloc(blurredHeight * context.bytesPerRow)
defer {
free(tempBufferBytes)
}
let temp2BufferBytes = malloc(blurredHeight * context.bytesPerRow)
defer {
free(temp2BufferBytes)
}
memset(temp2BufferBytes, Int32(bitPattern: color?.argb ?? 0xffffffff), blurredHeight * context.bytesPerRow)
var tempBuffer = vImage_Buffer()
tempBuffer.width = UInt(blurredWidth)
tempBuffer.height = UInt(blurredHeight)
tempBuffer.data = tempBufferBytes
tempBuffer.rowBytes = context.bytesPerRow
var temp2Buffer = vImage_Buffer()
temp2Buffer.width = UInt(blurredWidth)
temp2Buffer.height = UInt(blurredHeight)
temp2Buffer.data = temp2BufferBytes
temp2Buffer.rowBytes = context.bytesPerRow
vImageScale_ARGB8888(&sourceBuffer, &tempBuffer, nil, vImage_Flags(kvImageDoNotTile))
//vImageUnpremultiplyData_ARGB8888(&tempBuffer, &tempBuffer, vImage_Flags(kvImageDoNotTile))
vImagePremultipliedAlphaBlend_ARGB8888(&tempBuffer, &temp2Buffer, &destinationBuffer, vImage_Flags(kvImageDoNotTile))
//vImageCopyBuffer(&tempBuffer, &destinationBuffer, 4, vImage_Flags(kvImageDoNotTile))*/
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
}
self.blurredRepresentationValue = context.generateImage()
return self.blurredRepresentationValue
default:
return nil
}
}
}
static let queue = Queue(name: "ItemAnimationContext", qos: .default)
private let cache: AnimationCache
@ -135,11 +276,10 @@ private final class ItemAnimationContext {
private var disposable: Disposable?
private var displayLink: ConstantDisplayLinkAnimator?
private var timestamp: Double = 0.0
private var item: AnimationCacheItem?
private var currentFrameGroup: FrameGroup?
private var isLoadingFrameGroup: Bool = false
private var currentFrame: Frame?
private var isLoadingFrame: Bool = false
private(set) var isPlaying: Bool = false {
didSet {
@ -180,9 +320,13 @@ private final class ItemAnimationContext {
}
func updateAddedTarget(target: MultiAnimationRenderTarget) {
if let currentFrameGroup = self.currentFrameGroup {
if let cgImage = currentFrameGroup.image.cgImage {
if let currentFrame = self.currentFrame {
if let cgImage = currentFrame.image.cgImage {
target.transitionToContents(cgImage)
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
}
}
}
@ -215,35 +359,57 @@ private final class ItemAnimationContext {
return self.update(advanceTimestamp: advanceTimestamp)
}
private func update(advanceTimestamp: Double?) -> LoadFrameGroupTask? {
private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? {
guard let item = self.item else {
return nil
}
let timestamp = self.timestamp
if let advanceTimestamp = advanceTimestamp {
self.timestamp += advanceTimestamp
var frameAdvance: AnimationCacheItem.Advance?
if !self.isLoadingFrame {
if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 {
let divisionFactor = advanceTimestamp / currentFrame.remainingDuration
let wholeFactor = round(divisionFactor)
if abs(wholeFactor - divisionFactor) < 0.005 {
currentFrame.remainingDuration = 0.0
frameAdvance = .frames(Int(wholeFactor))
} else {
currentFrame.remainingDuration -= advanceTimestamp
if currentFrame.remainingDuration <= 0.0 {
frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration))
}
}
} else if self.currentFrame == nil {
frameAdvance = .frames(1)
}
}
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.timestamp == self.timestamp {
} else if !self.isLoadingFrameGroup {
self.isLoadingFrameGroup = true
if let frameAdvance = frameAdvance, !self.isLoadingFrame {
self.isLoadingFrame = true
return LoadFrameGroupTask(task: { [weak self] in
let currentFrameGroup = FrameGroup(item: item, timestamp: timestamp)
let currentFrame: Frame?
if let frame = item.advance(advance: frameAdvance, requestedFormat: .rgba) {
currentFrame = Frame(frame: frame)
} else {
currentFrame = nil
}
return {
guard let strongSelf = self else {
return
}
strongSelf.isLoadingFrameGroup = false
strongSelf.isLoadingFrame = false
if let currentFrameGroup = currentFrameGroup {
strongSelf.currentFrameGroup = currentFrameGroup
if let currentFrame = currentFrame {
strongSelf.currentFrame = currentFrame
for target in strongSelf.targets.copyItems() {
if let target = target.value {
target.transitionToContents(currentFrameGroup.image.cgImage!)
target.transitionToContents(currentFrame.image.cgImage!)
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
}
}
}
}
@ -251,7 +417,7 @@ private final class ItemAnimationContext {
})
}
if let _ = self.currentFrameGroup {
if let _ = self.currentFrame {
for target in self.targets.copyItems() {
if let target = target.value {
target.updateDisplayPlaceholder(displayPlaceholder: false)
@ -268,7 +434,13 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
private let firstFrameQueue: Queue
private let stateUpdated: () -> Void
private var itemContexts: [String: ItemAnimationContext] = [:]
private struct ItemKey: Hashable {
var id: String
var width: Int
var height: Int
}
private var itemContexts: [ItemKey: ItemAnimationContext] = [:]
private(set) var isPlaying: Bool = false {
didSet {
@ -284,8 +456,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height))
let itemContext: ItemAnimationContext
if let current = self.itemContexts[itemId] {
if let current = self.itemContexts[itemKey] {
itemContext = current
} else {
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
@ -294,7 +467,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
strongSelf.updateIsPlaying()
})
self.itemContexts[itemId] = itemContext
self.itemContexts[itemKey] = itemContext
}
let index = itemContext.targets.add(Weak(target))
@ -302,12 +475,12 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in
Queue.mainQueue().async {
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else {
return
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
strongSelf.itemContexts.removeValue(forKey: itemKey)
}
}
}
@ -320,7 +493,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
return ActionDisposable { [weak self, weak itemContext, weak target] in
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else {
return
}
if let target = target {
@ -329,18 +502,25 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
strongSelf.itemContexts.removeValue(forKey: itemKey)
}
}
}
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) {
guard let frameGroup = FrameGroup(item: item, timestamp: 0.0) else {
guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else {
return false
}
guard let loadedFrame = ItemAnimationContext.Frame(frame: frame) else {
return false
}
target.contents = frameGroup.image.cgImage
target.contents = loadedFrame.image.cgImage
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
}
return true
} else {
@ -357,15 +537,24 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
return
}
let frameGroup = FrameGroup(item: item, timestamp: 0.0)
let loadedFrame: ItemAnimationContext.Frame?
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
loadedFrame = ItemAnimationContext.Frame(frame: frame)
} else {
loadedFrame = nil
}
Queue.mainQueue().async {
guard let target = target else {
completion(false)
return
}
if let frameGroup = frameGroup {
target.contents = frameGroup.image.cgImage
if let loadedFrame = loadedFrame {
target.contents = loadedFrame.image.cgImage
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
}
completion(true)
} else {

View File

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

View File

@ -44,7 +44,8 @@ public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: A
}
}
}
}, proposedWidth: frame.width, proposedHeight: frame.height, duration: frameDuration)
return frameDuration
}, proposedWidth: frame.width, proposedHeight: frame.height)
} else {
break
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addstickers.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,103 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 8.000000 2.000000 cm
0.000000 0.000000 0.000000 scn
1.000000 10.000000 m
1.000000 10.552285 0.552285 11.000000 0.000000 11.000000 c
-0.552285 11.000000 -1.000000 10.552285 -1.000000 10.000000 c
1.000000 10.000000 l
h
-1.000000 2.000000 m
-1.000000 1.447715 -0.552285 1.000000 0.000000 1.000000 c
0.552285 1.000000 1.000000 1.447715 1.000000 2.000000 c
-1.000000 2.000000 l
h
-1.000000 10.000000 m
-1.000000 2.000000 l
1.000000 2.000000 l
1.000000 10.000000 l
-1.000000 10.000000 l
h
f
n
Q
q
0.000000 1.000000 -1.000000 0.000000 6.000000 8.000000 cm
0.000000 0.000000 0.000000 scn
1.000000 2.000000 m
1.000000 2.552285 0.552285 3.000000 0.000000 3.000000 c
-0.552285 3.000000 -1.000000 2.552285 -1.000000 2.000000 c
1.000000 2.000000 l
h
-1.000000 -6.000000 m
-1.000000 -6.552285 -0.552285 -7.000000 0.000000 -7.000000 c
0.552285 -7.000000 1.000000 -6.552285 1.000000 -6.000000 c
-1.000000 -6.000000 l
h
-1.000000 2.000000 m
-1.000000 -6.000000 l
1.000000 -6.000000 l
1.000000 2.000000 l
-1.000000 2.000000 l
h
f
n
Q
endstream
endobj
3 0 obj
1083
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001173 00000 n
0000001196 00000 n
0000001369 00000 n
0000001443 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1502
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "lockedstickers.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.000000 2.999512 cm
0.000000 0.000000 0.000000 scn
2.165000 7.500039 m
2.165000 8.513481 2.986557 9.335039 4.000000 9.335039 c
5.013443 9.335039 5.835000 8.513481 5.835000 7.500039 c
5.835000 5.996800 l
5.637455 6.000156 5.413168 6.000156 5.155556 6.000156 c
2.844445 6.000156 l
2.586832 6.000156 2.362546 6.000156 2.165000 5.996800 c
2.165000 7.500039 l
h
0.835000 5.729585 m
0.835000 7.500039 l
0.835000 9.248020 2.252019 10.665039 4.000000 10.665039 c
5.747981 10.665039 7.165000 9.248020 7.165000 7.500039 c
7.165000 5.729585 l
7.437430 5.559180 7.659470 5.317513 7.806234 5.029473 c
8.000000 4.649185 8.000000 4.151361 8.000000 3.155712 c
8.000000 2.844601 l
8.000000 1.848951 8.000000 1.351128 7.806234 0.970840 c
7.635793 0.636330 7.363827 0.364364 7.029317 0.193922 c
6.649029 0.000156 6.151205 0.000156 5.155556 0.000156 c
2.844444 0.000156 l
1.848796 0.000156 1.350971 0.000156 0.970684 0.193922 c
0.636173 0.364364 0.364208 0.636330 0.193766 0.970840 c
0.000000 1.351128 0.000000 1.848951 0.000000 2.844601 c
0.000000 3.155712 l
0.000000 4.151361 0.000000 4.649185 0.193766 5.029473 c
0.340530 5.317513 0.562570 5.559180 0.835000 5.729585 c
h
f*
n
Q
endstream
endobj
3 0 obj
1229
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001319 00000 n
0000001342 00000 n
0000001515 00000 n
0000001589 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1648
%%EOF

View File

@ -147,6 +147,10 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
}
}
func ready() -> Signal<Bool, NoError> {
return .single(true)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let imageLayout = self.imageNode.asyncLayout()
let currentImageResource = self.currentImageResource

View File

@ -8606,6 +8606,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
}
strongSelf.chatDisplayNode.updateTypingActivity(true)
}, backwardsDeleteText: { [weak self] in
guard let strongSelf = self else {
return

View File

@ -2836,12 +2836,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText) {
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil))
} else {
var inlineStickers: [MediaId: Media] = [:]
effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let file = value.file {
inlineStickers[file.fileId] = file
}
}
})
let inputText = convertMarkdownToAttributes(effectiveInputText)
for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 {
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0/*Int(self.context.userLimits.maxAnimatedEmojisInText)*/))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
@ -2852,17 +2861,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
webpage = self.chatPresentationInterfaceState.urlPreview?.1
}
messages.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil))
#if DEBUG
if text.string == "sleep" {
messages.removeAll()
for i in 0 ..< 5 {
messages.append(.message(text: "sleep\(i)", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil))
}
}
#endif
messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil))
}
}

View File

@ -79,19 +79,66 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.LocalRecentEmoji], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium,
context.account.viewTracker.featuredEmojiPacks()
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
|> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var isPremium: Bool
var title: String
var isPremiumLocked: Bool
var isFeatured: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
var recentEmoji: OrderedItemListView?
for orderedView in view.orderedItemListsViews {
if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji {
recentEmoji = orderedView
}
}
if let recentEmoji = recentEmoji {
for item in recentEmoji.items {
guard let item = item.contents.get(RecentEmojiItem.self) else {
continue
}
if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji {
continue
}
let resultItem: EmojiPagerContentComponent.Item
switch item.content {
case let .file(file):
resultItem = EmojiPagerContentComponent.Item(
file: file,
staticEmoji: nil,
subgroupId: nil
)
case let .text(text):
resultItem = EmojiPagerContentComponent.Item(
file: nil,
staticEmoji: text,
subgroupId: nil
)
}
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, items: [resultItem]))
}
}
}
for (subgroupId, list) in staticEmojiMapping {
let groupId: AnyHashable = "static"
for emojiString in list {
@ -105,11 +152,17 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Emoji", isPremiumLocked: false, isFeatured: false, items: [resultItem]))
}
}
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in view.collectionInfos {
installedCollectionIds.insert(id)
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
@ -122,20 +175,50 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let supergroupId = entry.index.collectionId
let groupId: AnyHashable = supergroupId
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremium && isPremiumDisabled {
let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremiumLocked && 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]))
var title = ""
inner: for (id, info, _) in view.collectionInfos {
if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo {
title = info.title
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, isPremiumLocked: isPremiumLocked, isFeatured: false, items: [resultItem]))
}
}
for featuredEmojiPack in featuredEmojiPacks {
if installedCollectionIds.contains(featuredEmojiPack.info.id) {
continue
}
for item in featuredEmojiPack.topItems {
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = featuredEmojiPack.info.id
let groupId: AnyHashable = supergroupId
let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremiumLocked && isPremiumDisabled {
continue
}
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, isPremiumLocked: isPremiumLocked, isFeatured: true, items: [resultItem]))
}
}
}
@ -146,20 +229,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
animationRenderer: animationRenderer,
inputInteraction: inputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var title: String?
var hasClear = false
if group.id == AnyHashable("recent") {
//TODO:localize
title = "Recently Used"
} else {
for (id, info, _) in view.collectionInfos {
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
title = info.title
break
}
}
hasClear = true
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: false, items: group.items)
},
itemLayoutType: .compact
)
@ -244,12 +319,53 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
},
openStickerSettings: {
},
openPremiumSection: { [weak controllerInteraction] in
addGroupAction: { [weak controllerInteraction] groupId, isPremiumLocked in
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
return
}
if isPremiumLocked {
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredEmojiPack.info.id == collectionId {
let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()
break
}
}
})
},
clearGroup: { [weak controllerInteraction] groupId in
guard let controllerInteraction = controllerInteraction else {
return
}
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
if groupId == AnyHashable("recent") {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.stickers.clearRecentlyUsedEmoji().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controllerInteraction.presentController(actionSheet, nil)
}
},
pushController: { [weak controllerInteraction] controller in
guard let controllerInteraction = controllerInteraction else {
@ -311,7 +427,27 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
controller.navigationPresentation = .modal
controllerInteraction.navigationController()?.pushViewController(controller)
},
openPremiumSection: {
addGroupAction: { _, _ in
},
clearGroup: { [weak controllerInteraction] groupId in
guard let controllerInteraction = controllerInteraction else {
return
}
if groupId == AnyHashable("recent") {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.stickers.clearRecentlyUsedStickers().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controllerInteraction.presentController(actionSheet, nil)
}
},
pushController: { [weak controllerInteraction] controller in
guard let controllerInteraction = controllerInteraction else {
@ -366,6 +502,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String
var isPremiumLocked: Bool
var isFeatured: Bool
var displayPremiumBadges: Bool
var items: [EmojiPagerContentComponent.Item]
}
@ -408,7 +547,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Saved", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -434,7 +574,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
count += 1
@ -483,7 +624,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem]))
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Premium", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -502,7 +644,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: true, items: [resultItem]))
var title = ""
inner: for (id, info, _) in view.collectionInfos {
if id == groupId, let info = info as? StickerPackCollectionInfo {
title = info.title
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, items: [resultItem]))
}
}
@ -513,26 +663,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
animationRenderer: animationRenderer,
inputInteraction: stickerInputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var title: String?
if group.id == AnyHashable("saved") {
//TODO:localize
title = "Saved"
} else if group.id == AnyHashable("recent") {
//TODO:localize
title = "Recently Used"
} else if group.id == AnyHashable("premium") {
//TODO:localize
title = "Premium"
} else {
for (id, info, _) in view.collectionInfos {
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
title = info.title
break
}
}
var hasClear = false
if group.id == AnyHashable("recent") {
hasClear = true
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: false, displayPremiumBadges: group.displayPremiumBadges, items: group.items)
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: group.displayPremiumBadges, items: group.items)
},
itemLayoutType: .detailed
)
@ -592,6 +728,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: nil
))
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
return combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
@ -603,7 +740,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = []
for reaction in reactions {
if let file = animatedEmojiStickers[reaction]?.first?.file {
availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: reaction))
var title: String?
switch reaction {
case "😡":
title = strings.Gif_Emotion_Angry
case "😮":
title = strings.Gif_Emotion_Surprised
case "😂":
title = strings.Gif_Emotion_Joy
case "😘":
title = strings.Gif_Emotion_Kiss
case "😍":
title = strings.Gif_Emotion_Hearts
case "👍":
title = strings.Gif_Emotion_ThumbsUp
case "👎":
title = strings.Gif_Emotion_ThumbsDown
case "🙄":
title = strings.Gif_Emotion_RollEyes
case "😎":
title = strings.Gif_Emotion_Cool
case "🥳":
title = strings.Gif_Emotion_Party
default:
break
}
guard let title = title else {
continue
}
availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: title))
}
}
@ -995,6 +1162,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
gifContent = nil
}
if let gifContentValue = gifContent {
if gifContentValue.items.isEmpty {
gifContent = nil
}
}
let entityKeyboardSize = self.entityKeyboardView.update(
transition: mappedTransition,
component: AnyComponent(EntityKeyboardComponent(
@ -1340,7 +1513,9 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
},
openStickerSettings: {
},
openPremiumSection: {
addGroupAction: { _, _ in
},
clearGroup: { _ in
},
pushController: { _ in
},

View File

@ -184,9 +184,9 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: !isEmoji ? "anim_stickertosmile" : "anim_smiletosticker",
colors: colors,
mode: .animateTransitionFromPrevious
),
colors: colors,
size: animationFrame.size
)),
environment: {},