Implement optional cached resource data streaming

This commit is contained in:
Ali 2019-12-24 20:22:38 +04:00
parent 2ec22db1ff
commit e868873425
9 changed files with 233 additions and 79 deletions

View File

@ -71,13 +71,24 @@ private protocol AnimatedStickerFrameSource: class {
var frameRate: Int { get }
var frameCount: Int { get }
func takeFrame() -> AnimatedStickerFrame
func takeFrame() -> AnimatedStickerFrame?
}
private final class AnimatedStickerFrameSourceWrapper {
let value: AnimatedStickerFrameSource
init(_ value: AnimatedStickerFrameSource) {
self.value = value
}
}
@available(iOS 9.0, *)
private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private let data: Data
private var data: Data
private var dataComplete: Bool
private let notifyUpdated: () -> Void
private var scratchBuffer: Data
let width: Int
let bytesPerRow: Int
@ -90,9 +101,11 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
var decodeBuffer: Data
var frameBuffer: Data
init?(queue: Queue, data: Data) {
init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) {
self.queue = queue
self.data = data
self.dataComplete = complete
self.notifyUpdated = notifyUpdated
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
var offset = 0
@ -152,7 +165,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
assert(self.queue.isCurrent())
}
func takeFrame() -> AnimatedStickerFrame {
func takeFrame() -> AnimatedStickerFrame? {
var frameData: Data?
var isLastFrame = false
@ -163,8 +176,24 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
let frameIndex = self.frameIndex
self.data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
if self.offset + 4 > dataLength {
if self.dataComplete {
self.frameIndex = 0
self.offset = self.initialOffset
self.frameBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
memset(bytes, 0, frameBufferLength)
}
}
return
}
var frameLength: Int32 = 0
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
if self.offset + 4 + Int(frameLength) > dataLength {
return
}
self.offset += 4
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
@ -194,7 +223,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
self.frameIndex += 1
self.offset += Int(frameLength)
if self.offset == dataLength {
if self.offset == dataLength && self.dataComplete {
isLastFrame = true
self.frameIndex = 0
self.offset = self.initialOffset
@ -204,7 +233,16 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
}
}
return AnimatedStickerFrame(data: frameData!, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
if let frameData = frameData {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
} else {
return nil
}
}
func updateData(data: Data, complete: Bool) {
self.data = data
self.dataComplete = complete
}
}
@ -241,7 +279,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
assert(self.queue.isCurrent())
}
func takeFrame() -> AnimatedStickerFrame {
func takeFrame() -> AnimatedStickerFrame? {
let frameIndex = self.currentFrame % self.frameCount
self.currentFrame += 1
var frameData = Data(count: self.bytesPerRow * self.height)
@ -271,15 +309,23 @@ private final class AnimatedStickerFrameQueue {
func take() -> AnimatedStickerFrame? {
if self.frames.isEmpty {
self.frames.append(self.source.takeFrame())
if let frame = self.source.takeFrame() {
self.frames.append(frame)
}
}
if !self.frames.isEmpty {
let frame = self.frames.removeFirst()
return frame
} else {
return nil
}
let frame = self.frames.removeFirst()
return frame
}
func generateFramesIfNeeded() {
if self.frames.isEmpty {
self.frames.append(self.source.takeFrame())
if let frame = self.source.takeFrame() {
self.frames.append(frame)
}
}
}
}
@ -297,7 +343,7 @@ public struct AnimatedStickerStatus: Equatable {
}
public protocol AnimatedStickerNodeSource {
func cachedDataPath(width: Int, height: Int) -> Signal<String, NoError>
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
func directDataPath() -> Signal<String, NoError>
}
@ -312,7 +358,7 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource
return .single(self.path)
}
public func cachedDataPath(width: Int, height: Int) -> Signal<String, NoError> {
public func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> {
return .never()
}
}
@ -330,9 +376,10 @@ public final class AnimatedStickerNode: ASDisplayNode {
private var reportedStarted = false
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
private var directData: (Data, String, Int, Int)?
private var cachedData: Data?
private var cachedData: (Data, Bool)?
private var renderer: (AnimationRenderer & ASDisplayNode)?
@ -421,15 +468,30 @@ public final class AnimatedStickerNode: ASDisplayNode {
}))
case .cached:
self.disposable.set((source.cachedDataPath(width: width, height: height)
|> deliverOnMainQueue).start(next: { [weak self] path in
|> deliverOnMainQueue).start(next: { [weak self] path, complete in
guard let strongSelf = self else {
return
}
strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead])
if strongSelf.isPlaying {
strongSelf.play()
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
if let (_, currentComplete) = strongSelf.cachedData {
if !currentComplete {
strongSelf.cachedData = (data, complete)
strongSelf.frameSource.with { frameSource in
frameSource?.with { frameSource in
if let frameSource = frameSource.value as? AnimatedStickerCachedFrameSource {
frameSource.updateData(data: data, complete: complete)
}
}
}
}
} else {
strongSelf.cachedData = (data, complete)
if strongSelf.isPlaying {
strongSelf.play()
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
}
}
}
}))
}
@ -464,15 +526,24 @@ public final class AnimatedStickerNode: ASDisplayNode {
let cachedData = self.cachedData
let queue = self.queue
let timerHolder = self.timer
let frameSourceHolder = self.frameSource
self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource?
var notifyUpdated: (() -> Void)?
if let directData = directData {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
} else if let cachedData = cachedData {
} else if let (cachedData, cachedDataComplete) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData)
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
notifyUpdated?()
})
}
}
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
return QueueLocalObject(queue: queue, generate: {
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
})
})
guard let frameSource = maybeFrameSource else {
return
}
@ -541,9 +612,9 @@ public final class AnimatedStickerNode: ASDisplayNode {
var maybeFrameSource: AnimatedStickerFrameSource?
if let directData = directData {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
} else if let cachedData = cachedData {
} else if let (cachedData, cachedDataComplete) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData)
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
}
}
guard let frameSource = maybeFrameSource else {

View File

@ -148,7 +148,7 @@ public final class WriteBuffer: MemoryBuffer {
self.offset = 0
}
public func write(_ data: UnsafeRawPointer, offset: Int, length: Int) {
public func write(_ data: UnsafeRawPointer, offset: Int = 0, length: Int) {
if self.offset + length > self.capacity {
self.capacity = self.offset + length + 256
if self.length == 0 {

View File

@ -85,6 +85,9 @@ public enum MediaResourceDataFetchError {
}
public enum CachedMediaResourceRepresentationResult {
case reset
case data(Data)
case done
case temporaryPath(String)
case tempFile(TempBoxFile)
}
@ -107,9 +110,19 @@ private struct CachedMediaResourceRepresentationKey: Hashable {
}
}
private final class CachedMediaResourceRepresentationSubscriber {
let update: (MediaResourceData) -> Void
let onlyComplete: Bool
init(update: @escaping (MediaResourceData) -> Void, onlyComplete: Bool) {
self.update = update
self.onlyComplete = onlyComplete
}
}
private final class CachedMediaResourceRepresentationContext {
var currentData: MediaResourceData?
let dataSubscribers = Bag<(MediaResourceData) -> Void>()
let dataSubscribers = Bag<CachedMediaResourceRepresentationSubscriber>()
let disposable = MetaDisposable()
var initialized = false
}
@ -191,7 +204,7 @@ public final class MediaBox {
return ResourceStorePaths(partial: "\(self.basePath)/\(fileNameForId(id))_partial", complete: "\(self.basePath)/\(fileNameForId(id))")
}
private func cachedRepresentationPathForId(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> String {
private func cachedRepresentationPathsForId(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> ResourceStorePaths {
let cacheString: String
switch representation.keepDuration {
case .general:
@ -199,7 +212,7 @@ public final class MediaBox {
case .shortLived:
cacheString = "short-cache"
}
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)"
return ResourceStorePaths(partial: "\(self.basePath)/\(cacheString)/\(fileNameForId(id))_partial:\(representation.uniqueId)", complete: "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)")
}
public func storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool = false) {
@ -662,7 +675,7 @@ public final class MediaBox {
public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) {
self.dataQueue.async {
let path = self.cachedRepresentationPathForId(resource.id, representation: representation)
let path = self.cachedRepresentationPathsForId(resource.id, representation: representation).complete
let _ = try? data.write(to: URL(fileURLWithPath: path))
}
}
@ -672,26 +685,26 @@ public final class MediaBox {
let disposable = MetaDisposable()
let begin: () -> Void = {
let path = self.cachedRepresentationPathForId(resource.id, representation: representation)
if let size = fileSize(path) {
let paths = self.cachedRepresentationPathsForId(resource.id, representation: representation)
if let size = fileSize(paths.complete) {
self.timeBasedCleanup.touch(paths: [
path
paths.complete
])
if let pathExtension = pathExtension {
let symlinkPath = path + ".\(pathExtension)"
let symlinkPath = paths.complete + ".\(pathExtension)"
if fileSize(symlinkPath) == nil {
let _ = try? FileManager.default.linkItem(atPath: path, toPath: symlinkPath)
let _ = try? FileManager.default.linkItem(atPath: paths.complete, toPath: symlinkPath)
}
subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: size, complete: true))
subscriber.putCompletion()
} else {
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: size, complete: true))
subscriber.putNext(MediaResourceData(path: paths.complete, offset: 0, size: size, complete: true))
subscriber.putCompletion()
}
} else if fetch {
if attemptSynchronously && complete {
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false))
}
self.dataQueue.async {
let key = CachedMediaResourceRepresentationKey(resourceId: resource.id, representation: representation)
@ -703,7 +716,7 @@ public final class MediaBox {
self.cachedRepresentationContexts[key] = context
}
let index = context.dataSubscribers.add(({ data in
let index = context.dataSubscribers.add(CachedMediaResourceRepresentationSubscriber(update: { data in
if !complete || data.complete {
if let pathExtension = pathExtension, data.complete {
let symlinkPath = data.path + ".\(pathExtension)"
@ -718,7 +731,7 @@ public final class MediaBox {
if data.complete {
subscriber.putCompletion()
}
}))
}, onlyComplete: complete))
if let currentData = context.currentData {
if !complete || currentData.complete {
subscriber.putNext(currentData)
@ -727,7 +740,7 @@ public final class MediaBox {
subscriber.putCompletion()
}
} else if !complete {
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false))
}
disposable.set(ActionDisposable { [weak context] in
@ -752,32 +765,65 @@ public final class MediaBox {
}
|> deliverOn(self.dataQueue)
context.disposable.set(signal.start(next: { [weak self, weak context] next in
guard let strongSelf = self else {
return
}
if let next = next {
var isDone = false
switch next {
case let .temporaryPath(temporaryPath):
rename(temporaryPath, path)
case let .tempFile(tempFile):
rename(tempFile.path, path)
TempBox.shared.dispose(tempFile)
case let .temporaryPath(temporaryPath):
rename(temporaryPath, paths.complete)
isDone = true
case let .tempFile(tempFile):
rename(tempFile.path, paths.complete)
TempBox.shared.dispose(tempFile)
isDone = true
case .reset:
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite)
file?.truncate(count: 0)
unlink(paths.complete)
case let .data(dataPart):
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append)
let dataCount = dataPart.count
dataPart.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
file?.write(bytes, count: dataCount)
}
case .done:
link(paths.partial, paths.complete)
isDone = true
}
if let strongSelf = self, let currentContext = strongSelf.cachedRepresentationContexts[key], currentContext === context {
currentContext.disposable.dispose()
strongSelf.cachedRepresentationContexts.removeValue(forKey: key)
if let size = fileSize(path) {
let data = MediaResourceData(path: path, offset: 0, size: size, complete: true)
if isDone {
currentContext.disposable.dispose()
strongSelf.cachedRepresentationContexts.removeValue(forKey: key)
}
if let size = fileSize(paths.complete) {
let data = MediaResourceData(path: paths.complete, offset: 0, size: size, complete: isDone)
currentContext.currentData = data
for subscriber in currentContext.dataSubscribers.copyItems() {
subscriber(data)
if !subscriber.onlyComplete || isDone {
subscriber.update(data)
}
}
} else if let size = fileSize(paths.partial) {
let data = MediaResourceData(path: paths.partial, offset: 0, size: size, complete: isDone)
currentContext.currentData = data
for subscriber in currentContext.dataSubscribers.copyItems() {
if !subscriber.onlyComplete || isDone {
subscriber.update(data)
}
}
}
}
} else {
if let strongSelf = self, let context = strongSelf.cachedRepresentationContexts[key] {
let data = MediaResourceData(path: path, offset: 0, size: 0, complete: false)
let data = MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false)
context.currentData = data
for subscriber in context.dataSubscribers.copyItems() {
subscriber(data)
if !subscriber.onlyComplete {
subscriber.update(data)
}
}
}
}
@ -785,7 +831,7 @@ public final class MediaBox {
}
}
} else {
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false))
subscriber.putCompletion()
}
}

View File

@ -166,8 +166,28 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese
}
switch next {
case let .result(_, items, _):
case let .result(info, items, _):
var preloadSignals: [Signal<Bool, NoError>] = []
if let thumbnail = info.thumbnail {
let signal = Signal<Bool, NoError> { subscriber in
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start()
let data = account.postbox.mediaBox.resourceData(thumbnail.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in
if data.complete {
subscriber.putNext(true)
subscriber.putCompletion()
} else {
subscriber.putNext(false)
}
})
return ActionDisposable {
fetched.dispose()
data.dispose()
}
}
preloadSignals.append(signal)
}
let topItems = items.prefix(16)
for item in topItems {
if let item = item as? StickerPackItem, item.file.isAnimatedSticker {

View File

@ -183,13 +183,12 @@ private let threadPool: ThreadPool = {
}()
@available(iOS 9.0, *)
public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<String, NoError> {
public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<Data, NoError> {
return Signal({ subscriber in
let cancelled = Atomic<Bool>(value: false)
threadPool.addTask(ThreadPoolTask({ _ in
if cancelled.with({ $0 }) {
//print("cancelled 1")
return
}
@ -206,12 +205,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
let endFrame = Int(player.frameCount)
if cancelled.with({ $0 }) {
//print("cancelled 2")
return
}
let path = NSTemporaryDirectory() + "\(arc4random64()).lz4v"
guard let fileContext = ManagedFile(queue: nil, path: path, mode: .readwrite) else {
return
}
@ -219,16 +212,19 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
var currentFrame: Int32 = 0
let writeBuffer = WriteBuffer()
var numberOfFramesCommitted = 0
var fps: Int32 = player.frameRate
var frameCount: Int32 = player.frameCount
let _ = fileContext.write(&fps, count: 4)
let _ = fileContext.write(&frameCount, count: 4)
writeBuffer.write(&fps, length: 4)
writeBuffer.write(&frameCount, length: 4)
var widthValue: Int32 = Int32(size.width)
var heightValue: Int32 = Int32(size.height)
var bytesPerRowValue: Int32 = Int32(bytesPerRow)
let _ = fileContext.write(&widthValue, count: 4)
let _ = fileContext.write(&heightValue, count: 4)
let _ = fileContext.write(&bytesPerRowValue, count: 4)
writeBuffer.write(&widthValue, length: 4)
writeBuffer.write(&heightValue, length: 4)
writeBuffer.write(&bytesPerRowValue, length: 4)
let frameLength = bytesPerRow * Int(size.height)
assert(frameLength % 16 == 0)
@ -262,7 +258,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
while currentFrame < endFrame {
if cancelled.with({ $0 }) {
//print("cancelled 3")
return
}
@ -298,8 +293,8 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
let length = compression_encode_buffer(bytes, compressedFrameDataLength, previousYuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
var frameLengthValue: Int32 = Int32(length)
let _ = fileContext.write(&frameLengthValue, count: 4)
let _ = fileContext.write(bytes, count: length)
writeBuffer.write(&frameLengthValue, length: 4)
writeBuffer.write(bytes, length: length)
}
let tmp = previousYuvaFrameData
@ -309,16 +304,33 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
compressionTime += CACurrentMediaTime() - compressionStartTime
currentFrame += 1
numberOfFramesCommitted += 1
if numberOfFramesCommitted >= 5 {
numberOfFramesCommitted = 0
subscriber.putNext(writeBuffer.makeData())
writeBuffer.reset()
/*#if DEBUG
usleep(500000)
#endif*/
}
}
if writeBuffer.length != 0 {
subscriber.putNext(writeBuffer.makeData())
}
subscriber.putNext(path)
subscriber.putCompletion()
print("animation render time \(CACurrentMediaTime() - startTime)")
/*print("animation render time \(CACurrentMediaTime() - startTime)")
print("of which drawing time \(drawingTime)")
print("of which appending time \(appendingTime)")
print("of which delta time \(deltaTime)")
print("of which compression time \(compressionTime)")
print("of which compression time \(compressionTime)")*/
}
}
}))

View File

@ -18,13 +18,13 @@ public final class AnimatedStickerResourceSource: AnimatedStickerNodeSource {
self.fitzModifier = fitzModifier
}
public func cachedDataPath(width: Int, height: Int) -> Signal<String, NoError> {
public func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> {
return chatMessageAnimationData(postbox: self.account.postbox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false)
|> filter { data in
return data.complete
return data.size != 0
}
|> map { data -> String in
return data.path
|> map { data -> (String, Bool) in
return (data.path, data.complete)
}
}

View File

@ -855,8 +855,11 @@ private func fetchAnimatedStickerRepresentation(account: Account, resource: Medi
return Signal({ subscriber in
if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) {
if #available(iOS 9.0, *) {
return experimentalConvertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.uniqueId)-\(representation.uniqueId)").start(next: { path in
subscriber.putNext(.temporaryPath(path))
subscriber.putNext(.reset)
return experimentalConvertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.uniqueId)-\(representation.uniqueId)").start(next: { data in
subscriber.putNext(.data(data))
}, completed: {
subscriber.putNext(.done)
subscriber.putCompletion()
})
} else {

View File

@ -148,7 +148,7 @@ class StickerPanePeerSpecificSetupGridItemNode: GridItemNode {
let textSpacing: CGFloat = 3.0
let buttonSpacing: CGFloat = 6.0
let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupChooseStickerPack, font: buttonFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupChooseStickerPack, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupStickers.uppercased(), font: titleFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))

View File

@ -530,6 +530,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
})
}
self.animatedStickerNode?.visibility = true
self.checkTimer()
}