Emoji input improvements

This commit is contained in:
Ali 2022-07-15 03:37:03 +02:00
parent 0b872d86c5
commit 0577baac79
35 changed files with 1413 additions and 694 deletions

View File

@ -108,6 +108,33 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground
}
}
private final class EntityInputView: UIInputView, UIInputViewAudioFeedback {
override var inputViewStyle: UIInputView.Style {
get {
return .default
}
}
override var allowsSelfSizing: Bool {
get {
return true
}
set {
}
}
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: 300)
}
override func sizeToFit() {
print("sizeToFit")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: 100.0)
}
}
private class CaptionEditableTextNode: EditableTextNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let previousAlpha = self.alpha
@ -437,6 +464,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
textInputNode.view.addGestureRecognizer(recognizer)
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
/*let entityInputView = EntityInputView()
entityInputView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))
entityInputView.backgroundColor = .blue
textInputNode.textView.inputView = entityInputView*/
}
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {

View File

@ -656,6 +656,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
var textFrame = self.textFieldFrame
textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel)
textFrame.size.height = self.textInputNode.textView.contentSize.height
textFrame.size.width -= self.textInputNode.textContainerInset.right
if self.textInputNode.isRTL {
textFrame.origin.x -= messageOriginDelta

View File

@ -381,9 +381,10 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
if component.contents.contains(where: { $0.id == defaultId }) {
centralId = defaultId
}
} else {
centralId = component.contents.first?.id
}
if centralId == nil {
centralId = component.contents.first?.id
}
}
if self.centralId != centralId {

View File

@ -335,6 +335,7 @@ open class BlurredBackgroundView: UIView {
if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
sublayer.backgroundColor = nil
sublayer.isOpaque = false
//sublayer.setValue(true as NSNumber, forKey: "allowsInPlaceFiltering")
let allowedKeys: [String] = [
"colorSaturate",
"gaussianBlur"

View File

@ -147,6 +147,7 @@ public final class TextNodeLayoutArguments {
public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)?
public let displaySpoilers: Bool
public let displayEmbeddedItemsUnderSpoilers: Bool
public init(
attributedString: NSAttributedString?,
@ -163,7 +164,8 @@ public final class TextNodeLayoutArguments {
lineColor: UIColor? = nil,
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil,
displaySpoilers: Bool = false
displaySpoilers: Bool = false,
displayEmbeddedItemsUnderSpoilers: Bool = false
) {
self.attributedString = attributedString
self.backgroundColor = backgroundColor
@ -180,6 +182,7 @@ public final class TextNodeLayoutArguments {
self.textShadowColor = textShadowColor
self.textStroke = textStroke
self.displaySpoilers = displaySpoilers
self.displayEmbeddedItemsUnderSpoilers = displayEmbeddedItemsUnderSpoilers
}
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
@ -198,7 +201,8 @@ public final class TextNodeLayoutArguments {
lineColor: self.lineColor,
textShadowColor: self.textShadowColor,
textStroke: self.textStroke,
displaySpoilers: self.displaySpoilers
displaySpoilers: self.displaySpoilers,
displayEmbeddedItemsUnderSpoilers: self.displayEmbeddedItemsUnderSpoilers
)
}
}
@ -972,7 +976,7 @@ open class TextNode: ASDisplayNode {
}
}
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool) -> TextNodeLayout {
if let attributedString = attributedString {
let stringLength = attributedString.length
@ -1126,10 +1130,6 @@ open class TextNode: ASDisplayNode {
rightOffset = ceil(secondaryRightOffset)
}
if embeddedItems.count > 25 {
assert(true)
}
embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item))
}
@ -1140,9 +1140,6 @@ open class TextNode: ASDisplayNode {
isLastLine = true
}
if isLastLine {
if attributedString.string.hasPrefix("😀") {
assert(true)
}
if first {
first = false
} else {
@ -1224,12 +1221,6 @@ open class TextNode: ASDisplayNode {
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
@ -1238,6 +1229,16 @@ open class TextNode: ASDisplayNode {
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
headIndent = paragraphStyle.headIndent
}
if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) {
if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
}
}
}
}
@ -1311,12 +1312,6 @@ open class TextNode: ASDisplayNode {
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
@ -1325,6 +1320,16 @@ open class TextNode: ASDisplayNode {
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
headIndent = paragraphStyle.headIndent
}
if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) {
if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
}
}
}
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
@ -1586,11 +1591,11 @@ open class TextNode: ASDisplayNode {
if stringMatch {
layout = existingLayout
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers)
updated = true
}
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers)
updated = true
}
@ -2231,11 +2236,11 @@ open class TextView: UIView {
if stringMatch {
layout = existingLayout
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers)
updated = true
}
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers)
updated = true
}

View File

@ -87,6 +87,8 @@
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth];
authService.delegate = self;
[_authMtProto addMessageService:authService];
[_authMtProto resume];
}
}
else

View File

@ -91,6 +91,8 @@
requestService.forceBackgroundRequests = true;
[_sourceDatacenterMtProto addMessageService:requestService];
[_sourceDatacenterMtProto resume];
MTRequest *request = [[MTRequest alloc] init];
NSData *exportAuthRequestData = nil;
@ -130,6 +132,8 @@
requestService.forceBackgroundRequests = true;
[_destinationDatacenterMtProto addMessageService:requestService];
[_destinationDatacenterMtProto resume];
MTRequest *request = [[MTRequest alloc] init];
NSData *importAuthRequestData = [_context.serialization importAuthorization:dataId bytes:authData];

View File

@ -96,6 +96,8 @@
_requestService.forceBackgroundRequests = true;
[_mtProto addMessageService:_requestService];
[_mtProto resume];
MTRequest *request = [[MTRequest alloc] init];
NSData *getConfigData = nil;

View File

@ -171,9 +171,11 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64;
_sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context];
_shouldStayConnected = true;
_mtState |= MTProtoStatePaused;
[self setMtState:_mtState | MTProtoStatePaused];
}
return self;
}

View File

@ -532,12 +532,25 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef
return thumbnail
|> mapToSignal { thumbnailData in
return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath)
|> map { fullSize, reducedSize in
if !fullSize._1 && reducedSize._1 {
return Tuple(thumbnailData, reducedSize._0, false)
if synchronousLoad, let thumbnailData = thumbnailData {
return .single(Tuple(thumbnailData, nil, false))
|> then(
combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath)
|> map { fullSize, reducedSize in
if !fullSize._1 && reducedSize._1 {
return Tuple(thumbnailData, reducedSize._0, false)
}
return Tuple(thumbnailData, fullSize._0, fullSize._1)
}
)
} else {
return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath)
|> map { fullSize, reducedSize in
if !fullSize._1 && reducedSize._1 {
return Tuple(thumbnailData, reducedSize._0, false)
}
return Tuple(thumbnailData, fullSize._0, fullSize._1)
}
return Tuple(thumbnailData, fullSize._0, fullSize._1)
}
}
}

View File

@ -13,7 +13,7 @@ private struct ScanFilesResult {
var totalSize: UInt64 = 0
}
private func printOpenFiles() {
public func printOpenFiles() {
var flags: Int32 = 0
var fd: Int32 = 0
var buf = Data(count: Int(MAXPATHLEN) + 1)

View File

@ -197,24 +197,23 @@ open class TabBarControllerImpl: ViewController, TabBarController {
return
}
if strongSelf.selectedIndex == index {
let timestamp = CACurrentMediaTime()
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 = 0
}
if strongSelf.debugTapCounter.0 >= timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 += 1
}
if strongSelf.debugTapCounter.1 >= 10 {
strongSelf.debugTapCounter.1 = 0
strongSelf.controllers[index].tabBarItemDebugTapAction?()
}
let timestamp = CACurrentMediaTime()
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 = 0
}
if strongSelf.debugTapCounter.0 >= timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 += 1
}
if strongSelf.debugTapCounter.1 >= 10 {
strongSelf.debugTapCounter.1 = 0
strongSelf.controllers[index].tabBarItemDebugTapAction?()
}
if let validLayout = strongSelf.validLayout {
var updatedLayout = validLayout

View File

@ -591,6 +591,10 @@ private final class MultipartFetchManager {
if totalTime > 0.0 {
let speed = Double(totalByteCount) / totalTime
Logger.shared.log("MultipartFetch", "\(self.resource.id.stringRepresentation) \(speed) bytes/s")
#if DEBUG
self.checkState()
#endif
}
}
}

View File

@ -5,5 +5,6 @@
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow);
void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow);
void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow);
#endif /* YuvConversion_h */

View File

@ -97,3 +97,19 @@ void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const
error = vImageOverwriteChannels_ARGB8888(&srcA, &destArgb, &destArgb, 1 << 0, kvImageDoNotTile);
}
void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow) {
vImage_Buffer src;
src.data = (void *)inPlane;
src.width = inWidth;
src.height = inHeight;
src.rowBytes = inBytesPerRow;
vImage_Buffer dst;
dst.data = (void *)outPlane;
dst.width = outWidth;
dst.height = outHeight;
dst.rowBytes = outBytesPerRow;
vImageScale_Planar8(&src, &dst, nil, kvImageDoNotTile);
}

View File

@ -15,7 +15,7 @@ private func alignUp(size: Int, align: Int) -> Int {
public final class AnimationCacheItemFrame {
public enum RequestedFormat {
case rgba
case yuva(bytesPerRow: Int)
case yuva(rowAlignment: Int)
}
public final class Plane {
@ -50,11 +50,17 @@ public final class AnimationCacheItem {
public let numFrames: Int
private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?
private let getFrameIndexImpl: (Double) -> Int
private let getFrameDurationImpl: (Int) -> Double?
public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int) {
public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int, getFrameDurationImpl: @escaping (Int) -> Double?) {
self.numFrames = numFrames
self.getFrameImpl = getFrame
self.getFrameIndexImpl = getFrameIndexImpl
self.getFrameDurationImpl = getFrameDurationImpl
}
public func getFrameDuration(index: Int) -> Double? {
return self.getFrameDurationImpl(index)
}
public func getFrame(index: Int, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? {
@ -123,7 +129,7 @@ private func md5Hash(_ string: String) -> String {
}
}
private func itemSubpath(hashString: String) -> (directory: String, fileName: String) {
private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) {
assert(hashString.count == 32)
var directory = ""
@ -134,7 +140,7 @@ private func itemSubpath(hashString: String) -> (directory: String, fileName: St
directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)]))
}
return (directory, hashString)
return (directory, "\(hashString)_\(width)x\(height)")
}
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
@ -209,6 +215,213 @@ private func decompressData(data: Data, range: Range<Int>, decompressedSize: Int
return decompressedFrameData
}
private final class AnimationCacheItemWriterInternal {
struct CompressedResult {
var path: String
}
private struct FrameMetadata {
var offset: Int
var length: Int
var duration: Double
}
var isCancelled: Bool = false
private let decompressedPath: String
private let compressedPath: String
private var file: ManagedFile?
private var currentYUVASurface: ImageYUVA420?
private var currentDctData: DctData?
private var currentDctCoefficients: DctCoefficientsYUVA420?
private var contentLengthOffset: Int?
private var isFailed: Bool = false
private var isFinished: Bool = false
private var frames: [FrameMetadata] = []
private var contentLength: Int = 0
private let dctQuality: Int
init?(allocateTempFile: @escaping () -> String) {
self.dctQuality = 70
self.decompressedPath = allocateTempFile()
self.compressedPath = allocateTempFile()
guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else {
return nil
}
self.file = file
}
func add(with drawingBlock: (ImageYUVA420) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) {
if self.isFailed || self.isFinished {
return
}
guard !self.isFailed, !self.isFinished, let file = self.file else {
return
}
let width = roundUp(proposedWidth, multiple: 16)
let height = roundUp(proposedWidth, multiple: 16)
var isFirstFrame = false
let yuvaSurface: ImageYUVA420
if let current = self.currentYUVASurface {
if current.yPlane.width == width && current.yPlane.height == height {
yuvaSurface = current
} else {
self.isFailed = true
return
}
} else {
isFirstFrame = true
yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil)
self.currentYUVASurface = yuvaSurface
}
let dctCoefficients: DctCoefficientsYUVA420
if let current = self.currentDctCoefficients {
if current.yPlane.width == width && current.yPlane.height == height {
dctCoefficients = current
} else {
self.isFailed = true
return
}
} else {
dctCoefficients = DctCoefficientsYUVA420(width: width, height: height)
self.currentDctCoefficients = dctCoefficients
}
let dctData: DctData
if let current = self.currentDctData, current.quality == self.dctQuality {
dctData = current
} else {
dctData = DctData(quality: self.dctQuality)
self.currentDctData = dctData
}
drawingBlock(yuvaSurface)
yuvaSurface.dct(dctData: dctData, target: dctCoefficients)
if isFirstFrame {
file.write(2 as UInt32)
file.write(UInt32(dctCoefficients.yPlane.width))
file.write(UInt32(dctCoefficients.yPlane.height))
file.write(UInt32(dctData.quality))
self.contentLengthOffset = Int(file.position())
file.write(0 as UInt32)
}
let framePosition = Int(file.position())
assert(framePosition >= 0)
var frameLength = 0
for i in 0 ..< 4 {
let dctPlane: DctCoefficientPlane
switch i {
case 0:
dctPlane = dctCoefficients.yPlane
case 1:
dctPlane = dctCoefficients.uPlane
case 2:
dctPlane = dctCoefficients.vPlane
case 3:
dctPlane = dctCoefficients.aPlane
default:
preconditionFailure()
}
dctPlane.data.withUnsafeBytes { bytes in
let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count)
}
frameLength += dctPlane.data.count
}
self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration))
self.contentLength += frameLength
}
func finish() -> CompressedResult? {
var shouldComplete = false
outer: for _ in 0 ..< 1 {
if !self.isFinished {
self.isFinished = true
shouldComplete = true
guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else {
self.isFailed = true
break outer
}
assert(contentLengthOffset >= 0)
let metadataPosition = file.position()
file.seek(position: Int64(contentLengthOffset))
file.write(UInt32(self.contentLength))
file.seek(position: metadataPosition)
file.write(UInt32(self.frames.count))
for frame in self.frames {
file.write(UInt32(frame.offset))
file.write(UInt32(frame.length))
file.write(Float32(frame.duration))
}
if !self.frames.isEmpty {
} else {
self.isFailed = true
break outer
}
if !self.isFailed {
self.file = nil
file._unsafeClose()
guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else {
self.isFailed = true
break outer
}
guard let compressedData = compressData(data: uncompressedData) else {
self.isFailed = true
break outer
}
guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else {
self.isFailed = true
break outer
}
compressedFile.write(Int32(uncompressedData.count))
let _ = compressedFile.write(compressedData)
compressedFile._unsafeClose()
}
}
}
if shouldComplete {
let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath)
if !self.isFailed {
return CompressedResult(path: self.compressedPath)
} else {
let _ = try? FileManager.default.removeItem(atPath: self.compressedPath)
return nil
}
} else {
return nil
}
}
}
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
struct CompressedResult {
var animationPath: String
@ -246,7 +459,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
private let lock = Lock()
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
self.dctQuality = 67
self.dctQuality = 70
self.queue = queue
self.decompressedPath = allocateTempFile()
@ -286,7 +499,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
} else {
isFirstFrame = true
surface = ImageARGB(width: width, height: height, bytesPerRow: alignUp(size: width * 4, align: 32))
surface = ImageARGB(width: width, height: height, rowAlignment: 32)
self.currentSurface = surface
}
@ -299,7 +512,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
return
}
} else {
yuvaSurface = ImageYUVA420(width: width, height: height, bytesPerRow: nil)
yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil)
self.currentYUVASurface = yuvaSurface
}
@ -556,17 +769,17 @@ private final class AnimationCacheItemAccessor {
if let currentYUVASurface = self.currentYUVASurface {
yuvaSurface = currentYUVASurface
} else {
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: nil)
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: nil)
}
case let .yuva(preferredBytesPerRow):
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: preferredBytesPerRow)
case let .yuva(preferredRowAlignment):
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment)
}
self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface)
switch requestedFormat {
case .rgba:
let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, bytesPerRow: alignUp(size: yuvaSurface.yPlane.width * 4, align: 32))
let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, rowAlignment: 32)
yuvaSurface.toARGB(target: currentSurface)
self.currentYUVASurface = yuvaSurface
@ -619,6 +832,14 @@ private final class AnimationCacheItemAccessor {
}
return self.durationMapping.count - 1
}
func getFrameDuration(index: Int) -> Double? {
if index < self.durationMapping.count {
return self.durationMapping[index]
} else {
return nil
}
}
}
private func readUInt32(data: Data, offset: Int) -> UInt32 {
@ -747,9 +968,100 @@ private func loadItem(path: String) -> AnimationCacheItem? {
return itemAccessor.getFrame(index: index, requestedFormat: requestedFormat)
}, getFrameIndexImpl: { duration in
return itemAccessor.getFrameIndex(duration: duration)
}, getFrameDurationImpl: { index in
return itemAccessor.getFrameDuration(index: index)
})
}
private func adaptItemFromHigherResolution(itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
guard let higherResolutionItem = loadItem(path: higherResolutionPath) else {
return nil
}
guard let writer = AnimationCacheItemWriterInternal(allocateTempFile: allocateTempFile) else {
return nil
}
for i in 0 ..< higherResolutionItem.numFrames {
guard let duration = higherResolutionItem.getFrameDuration(index: i) else {
break
}
writer.add(with: { yuva in
guard let frame = higherResolutionItem.getFrame(index: i, requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else {
return
}
switch frame.format {
case .rgba:
return
case let .yuva(y, u, v, a):
yuva.yPlane.copyScaled(fromPlane: y)
yuva.uPlane.copyScaled(fromPlane: u)
yuva.vPlane.copyScaled(fromPlane: v)
yuva.aPlane.copyScaled(fromPlane: a)
}
}, proposedWidth: width, proposedHeight: height, duration: duration)
}
guard let result = writer.finish() else {
return nil
}
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
return nil
}
let _ = try? FileManager.default.removeItem(atPath: itemPath)
guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else {
return nil
}
guard let item = loadItem(path: itemPath) else {
return nil
}
return item
}
private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? {
var candidates: [(path: String, width: Int, height: Int)] = []
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) {
for url in enumerator {
guard let url = url as? URL else {
continue
}
let fileName = url.lastPathComponent
if fileName.hasPrefix(baseName) {
let scanner = Scanner(string: fileName)
guard scanner.scanString(baseName, into: nil) else {
continue
}
var itemWidth: Int = 0
guard scanner.scanInt(&itemWidth) else {
continue
}
guard scanner.scanString("x", into: nil) else {
continue
}
var itemHeight: Int = 0
guard scanner.scanInt(&itemHeight) else {
continue
}
if !baseSuffix.isEmpty {
guard scanner.scanString(baseSuffix, into: nil) else {
continue
}
}
guard scanner.isAtEnd else {
continue
}
if itemWidth > width && itemHeight > height {
candidates.append((url.path, itemWidth, itemHeight))
}
}
}
}
if !candidates.isEmpty {
candidates.sort(by: { $0.width < $1.width })
return candidates[0].path
}
return nil
}
public final class AnimationCacheImpl: AnimationCache {
private final class Impl {
private final class ItemContext {
@ -789,7 +1101,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
@ -878,43 +1190,58 @@ public final class AnimationCacheImpl: AnimationCache {
}
}
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
return loadItem(path: itemFirstFramePath)
} else {
return nil
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
return adaptedItem
}
}
return nil
}
static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) {
completion(item)
return EmptyDisposable
} else {
completion(nil)
return EmptyDisposable
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
completion(adaptedItem)
return EmptyDisposable
}
}
completion(nil)
return EmptyDisposable
}
}
private let queue: Queue
private let basePath: String
private let impl: QueueLocalObject<Impl>
private let allocateTempFile: () -> String
public init(basePath: String, allocateTempFile: @escaping () -> String) {
let queue = Queue()
self.queue = queue
self.basePath = basePath
self.allocateTempFile = allocateTempFile
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
})
@ -939,15 +1266,16 @@ public final class AnimationCacheImpl: AnimationCache {
}
public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size)
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile)
}
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let disposable = MetaDisposable()
let basePath = self.basePath
let allocateTempFile = self.allocateTempFile
queue.async {
disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, completion: completion))
disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, completion: completion))
}
return disposable

View File

@ -2,27 +2,46 @@ import Foundation
import UIKit
import ImageDCT
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class ImagePlane {
let width: Int
let height: Int
let bytesPerRow: Int
let rowAlignment: Int
let components: Int
var data: Data
init(width: Int, height: Int, components: Int, bytesPerRow: Int?) {
init(width: Int, height: Int, components: Int, rowAlignment: Int?) {
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow ?? (width * components)
self.rowAlignment = rowAlignment ?? 1
self.bytesPerRow = alignUp(size: width * components, align: self.rowAlignment)
self.components = components
self.data = Data(count: self.bytesPerRow * height)
}
}
extension ImagePlane {
func copyScaled(fromPlane plane: AnimationCacheItemFrame.Plane) {
self.data.withUnsafeMutableBytes { destBytes in
plane.data.withUnsafeBytes { srcBytes in
scaleImagePlane(destBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(self.width), Int32(self.height), Int32(self.bytesPerRow), srcBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(plane.width), Int32(plane.height), Int32(plane.bytesPerRow))
}
}
}
}
final class ImageARGB {
let argbPlane: ImagePlane
init(width: Int, height: Int, bytesPerRow: Int?) {
self.argbPlane = ImagePlane(width: width, height: height, components: 4, bytesPerRow: bytesPerRow)
init(width: Int, height: Int, rowAlignment: Int?) {
self.argbPlane = ImagePlane(width: width, height: height, components: 4, rowAlignment: rowAlignment)
}
}
@ -32,11 +51,11 @@ final class ImageYUVA420 {
let vPlane: ImagePlane
let aPlane: ImagePlane
init(width: Int, height: Int, bytesPerRow: Int?) {
self.yPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
self.aPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
init(width: Int, height: Int, rowAlignment: Int?) {
self.yPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.aPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
}
}
@ -92,8 +111,8 @@ extension ImageARGB {
}
}
func toYUVA420(bytesPerRow: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, bytesPerRow: bytesPerRow)
func toYUVA420(rowAlignment: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, rowAlignment: rowAlignment)
self.toYUVA420(target: resultImage)
return resultImage
}
@ -125,8 +144,8 @@ extension ImageYUVA420 {
}
}
func toARGB(bytesPerRow: Int?) -> ImageARGB {
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
func toARGB(rowAlignment: Int?) -> ImageARGB {
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
self.toARGB(target: resultImage)
return resultImage
}
@ -221,8 +240,8 @@ extension DctCoefficientsYUVA420 {
}
}
func idct(dctData: DctData, bytesPerRow: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
func idct(dctData: DctData, rowAlignment: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
self.idct(dctData: dctData, target: resultImage)
return resultImage
}

View File

@ -30,7 +30,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
private let context: AccountContext
private let groupId: String
private let emoji: ChatTextInputTextCustomEmojiAttribute
private let cache: AnimationCache
private let renderer: MultiAnimationRenderer
@ -39,6 +38,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private let pointSize: CGSize
private let pixelSize: CGSize
private var isDisplayingPlaceholder: Bool = false
private var file: TelegramMediaFile?
private var infoDisposable: Disposable?
private var disposable: Disposable?
@ -54,9 +55,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
}
public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) {
public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) {
self.context = context
self.groupId = groupId
self.emoji = emoji
self.cache = cache
self.renderer = renderer
@ -123,9 +123,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.file = file
if attemptSynchronousLoad {
if !self.renderer.loadFirstFrameSynchronously(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) {
if !self.renderer.loadFirstFrameSynchronously(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) {
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) {
self.contents = image.cgImage
self.isDisplayingPlaceholder = true
}
}
@ -133,10 +134,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
} else {
let pointSize = self.pointSize
let placeholderColor = self.placeholderColor
self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in
self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in
if !result {
MultiAnimationRendererImpl.firstFrameQueue.async {
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
DispatchQueue.main.async {
guard let strongSelf = self else {
@ -144,6 +145,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
if let image = image {
strongSelf.contents = image.cgImage
strongSelf.isDisplayingPlaceholder = true
}
strongSelf.loadAnimation()
}
@ -165,7 +167,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
let context = self.context
if file.isAnimatedSticker || file.isVideoEmoji {
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
@ -192,7 +194,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
})
} else {
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else {
return
@ -210,13 +212,45 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
})
}
}
override public func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.isDisplayingPlaceholder == displayPlaceholder {
return
}
self.isDisplayingPlaceholder = displayPlaceholder
}
override public func transitionToContents(_ contents: AnyObject) {
if self.isDisplayingPlaceholder {
self.isDisplayingPlaceholder = false
if let current = self.contents {
let previousLayer = SimpleLayer()
previousLayer.contents = current
previousLayer.frame = self.frame
self.superlayer?.insertSublayer(previousLayer, below: self)
previousLayer.opacity = 0.0
previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in
previousLayer?.removeFromSuperlayer()
})
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else {
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
self.contents = contents
}
}
}
public final class EmojiTextAttachmentView: UIView {
private let contentLayer: InlineStickerItemLayer
public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0))
self.contentLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0))
super.init(frame: CGRect())

View File

@ -537,12 +537,11 @@ public final class EmojiPagerContentComponent: Component {
}
}
private(set) var displayPlaceholder: Bool = false
let onUpdateDisplayPlaceholder: (Bool) -> Void
let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
init(
item: Item,
context: AccountContext,
groupId: String,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile?,
staticEmoji: String?,
@ -552,7 +551,7 @@ public final class EmojiPagerContentComponent: Component {
blurredBadgeColor: UIColor,
displayPremiumBadgeIfAvailable: Bool,
pointSize: CGSize,
onUpdateDisplayPlaceholder: @escaping (Bool) -> Void
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
) {
self.item = item
self.file = file
@ -573,7 +572,7 @@ public final class EmojiPagerContentComponent: Component {
return
}
strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
@ -604,13 +603,13 @@ public final class EmojiPagerContentComponent: Component {
}
if attemptSynchronousLoad {
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
self.updateDisplayPlaceholder(displayPlaceholder: true)
}
loadAnimation()
} else {
let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in
let _ = renderer.loadFirstFrame(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in
loadAnimation()
if !success {
@ -682,7 +681,7 @@ public final class EmojiPagerContentComponent: Component {
self.placeholderColor = layer.placeholderColor
self.size = layer.size
self.onUpdateDisplayPlaceholder = { _ in }
self.onUpdateDisplayPlaceholder = { _, _ in }
super.init(layer: layer)
}
@ -718,47 +717,17 @@ public final class EmojiPagerContentComponent: Component {
}
self.displayPlaceholder = displayPlaceholder
self.onUpdateDisplayPlaceholder(displayPlaceholder)
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
}
override func transitionToContents(_ contents: AnyObject) {
self.contents = contents
/*if displayPlaceholder {
if self.placeholderView == nil {
self.placeholderView = PortalView()
if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView {
self.addSublayer(placeholderView.view.layer)
placeholderView.view.frame = self.bounds
shimmerView.addPortal(view: placeholderView)
}
}
if self.placeholderMaskLayer == nil {
self.placeholderMaskLayer = SimpleLayer()
self.placeholderView?.view.layer.mask = self.placeholderMaskLayer
}
let file = self.file
let size = self.size
//let placeholderColor = self.placeholderColor
Queue.concurrentDefaultQueue().async { [weak self] in
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: .black) {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if strongSelf.displayPlaceholder {
strongSelf.placeholderMaskLayer?.contents = image.cgImage
}
}
}
}
} else {
if let placeholderView = self.placeholderView {
self.placeholderView = nil
placeholderView.view.layer.removeFromSuperlayer()
}
if let _ = self.placeholderMaskLayer {
self.placeholderMaskLayer = nil
}
}*/
if self.displayPlaceholder {
self.displayPlaceholder = false
self.onUpdateDisplayPlaceholder(false, 0.2)
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
}
@ -790,6 +759,7 @@ public final class EmojiPagerContentComponent: Component {
private let boundsChangeTrackerLayer = SimpleLayer()
private var effectiveVisibleSize: CGSize = CGSize()
private let placeholdersContainerView: UIView
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
@ -812,12 +782,13 @@ public final class EmojiPagerContentComponent: Component {
override init(frame: CGRect) {
self.shimmerHostView = PortalSourceView()
self.standaloneShimmerEffect = StandaloneShimmerEffect()
self.scrollView = ContentScrollView()
self.scrollView.layer.anchorPoint = CGPoint()
self.placeholdersContainerView = UIView()
super.init(frame: frame)
self.shimmerHostView.alpha = 0.0
@ -842,6 +813,8 @@ public final class EmojiPagerContentComponent: Component {
self.scrollView.clipsToBounds = false
self.addSubview(self.scrollView)
self.scrollView.addSubview(self.placeholdersContainerView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
@ -1404,7 +1377,6 @@ public final class EmojiPagerContentComponent: Component {
itemLayer = ItemLayer(
item: item,
context: component.context,
groupId: "keyboard-\(Int(itemLayout.nativeItemSize))",
attemptSynchronousLoad: attemptSynchronousLoads,
file: item.file,
staticEmoji: item.staticEmoji,
@ -1414,7 +1386,7 @@ public final class EmojiPagerContentComponent: Component {
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges,
pointSize: itemNativeFitSize,
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else {
return
}
@ -1432,7 +1404,7 @@ public final class EmojiPagerContentComponent: Component {
size: itemNativeFitSize
)
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
strongSelf.scrollView.insertSubview(placeholderView, at: 0)
strongSelf.placeholdersContainerView.addSubview(placeholderView)
}
placeholderView.frame = itemLayer.frame
placeholderView.update(size: placeholderView.bounds.size)
@ -1442,9 +1414,20 @@ public final class EmojiPagerContentComponent: Component {
} else {
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId)
placeholderView.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
if duration > 0.0 {
placeholderView.layer.opacity = 0.0
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in
guard let strongSelf = self else {
return
}
placeholderView?.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
})
} else {
placeholderView.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
}
}
}
}
@ -1471,7 +1454,7 @@ public final class EmojiPagerContentComponent: Component {
}
} else if updateItemLayerPlaceholder {
if itemLayer.displayPlaceholder {
itemLayer.onUpdateDisplayPlaceholder(true)
itemLayer.onUpdateDisplayPlaceholder(true, 0.0)
}
}
@ -1479,6 +1462,7 @@ public final class EmojiPagerContentComponent: Component {
}
}
var removedPlaceholerViews = false
var removedIds: [ItemLayer.Key] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
@ -1491,6 +1475,7 @@ public final class EmojiPagerContentComponent: Component {
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
view.removeFromSuperview()
removedPlaceholerViews = true
}
}
@ -1527,13 +1512,17 @@ public final class EmojiPagerContentComponent: Component {
self.visibleGroupPremiumButtons.removeValue(forKey: id)
}
if removedPlaceholerViews {
self.updateShimmerIfNeeded()
}
if let topVisibleGroupId = topVisibleGroupId {
self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate))
}
}
private func updateShimmerIfNeeded() {
if self.visibleItemPlaceholderViews.isEmpty {
if self.placeholdersContainerView.subviews.isEmpty {
self.standaloneShimmerEffect.layer = nil
} else {
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer

View File

@ -76,8 +76,8 @@ public final class EntityKeyboardComponent: Component {
public let theme: PresentationTheme
public let bottomInset: CGFloat
public let emojiContent: EmojiPagerContentComponent
public let stickerContent: EmojiPagerContentComponent
public let gifContent: GifPagerContentComponent
public let stickerContent: EmojiPagerContentComponent?
public let gifContent: GifPagerContentComponent?
public let availableGifSearchEmojies: [GifSearchEmoji]
public let defaultToEmojiTab: Bool
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
@ -94,8 +94,8 @@ public final class EntityKeyboardComponent: Component {
theme: PresentationTheme,
bottomInset: CGFloat,
emojiContent: EmojiPagerContentComponent,
stickerContent: EmojiPagerContentComponent,
gifContent: GifPagerContentComponent,
stickerContent: EmojiPagerContentComponent?,
gifContent: GifPagerContentComponent?,
availableGifSearchEmojies: [GifSearchEmoji],
defaultToEmojiTab: Bool,
externalTopPanelContainer: PagerExternalTopPanelContainer?,
@ -201,170 +201,179 @@ public final class EntityKeyboardComponent: Component {
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent)))
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
//TODO:localize
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "recent",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/RecentTabIcon",
theme: component.theme,
title: "Recent",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.recent)
}
))
))
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "trending",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/TrendingGifs",
theme: component.theme,
title: "Trending",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.trending)
}
))
))
for emoji in component.availableGifSearchEmojies {
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
if transition.userData(MarkInputCollapsed.self) != nil {
self.searchComponent = nil
}
if let gifContent = component.gifContent {
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent)))
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
//TODO:localize
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: emoji.emoji,
id: "recent",
isReorderable: false,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.stickerContent.context,
file: emoji.file,
animationCache: component.stickerContent.animationCache,
animationRenderer: component.stickerContent.animationRenderer,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/RecentTabIcon",
theme: component.theme,
title: emoji.title,
title: "Recent",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji))
self?.component?.switchToGifSubject(.recent)
}
))
))
}
let defaultActiveGifItemId: AnyHashable
switch component.gifContent.subject {
case .recent:
defaultActiveGifItemId = "recent"
case .trending:
defaultActiveGifItemId = "trending"
case let .emojiSearch(value):
defaultActiveGifItemId = AnyHashable(value)
}
contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topGifItems,
defaultActiveItemId: defaultActiveGifItemId,
activeContentItemIdUpdated: gifsContentItemIdUpdated,
reorderItems: { _ in
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "trending",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/TrendingGifs",
theme: component.theme,
title: "Trending",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.trending)
}
))
))
for emoji in component.availableGifSearchEmojies {
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: emoji.emoji,
isReorderable: false,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context,
file: emoji.file,
animationCache: component.emojiContent.animationCache,
animationRenderer: component.emojiContent.animationRenderer,
theme: component.theme,
title: emoji.title,
pressed: { [weak self] in
self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji))
}
))
))
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputGifsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
))))
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSearchIcon",
let defaultActiveGifItemId: AnyHashable
switch gifContent.subject {
case .recent:
defaultActiveGifItemId = "recent"
case .trending:
defaultActiveGifItemId = "trending"
case let .emojiSearch(value):
defaultActiveGifItemId = AnyHashable(value)
}
contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topGifItems,
defaultActiveItemId: defaultActiveGifItemId,
activeContentItemIdUpdated: gifsContentItemIdUpdated,
reorderItems: { _ in
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputGifsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
self?.openSearch()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in component.stickerContent.itemGroups {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: String] = [
"saved": "Chat/Input/Media/SavedStickersTabIcon",
"recent": "Chat/Input/Media/RecentTabIcon",
"premium": "Chat/Input/Media/PremiumIcon"
]
let titleMapping: [String: String] = [
"saved": "Saved",
"recent": "Recent",
"premium": "Premium"
]
if let iconName = iconMapping[id], let title = titleMapping[id] {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: iconName,
theme: component.theme,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
))))
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSearchIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
self?.openSearch()
}
} else {
if !itemGroup.items.isEmpty {
if let file = itemGroup.items[0].file {
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
if let stickerContent = component.stickerContent {
var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in stickerContent.itemGroups {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: String] = [
"saved": "Chat/Input/Media/SavedStickersTabIcon",
"recent": "Chat/Input/Media/RecentTabIcon",
"premium": "Chat/Input/Media/PremiumIcon"
]
let titleMapping: [String: String] = [
"saved": "Saved",
"recent": "Recent",
"premium": "Premium"
]
if let iconName = iconMapping[id], let title = titleMapping[id] {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: true,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.stickerContent.context,
file: file,
animationCache: component.stickerContent.animationCache,
animationRenderer: component.stickerContent.animationRenderer,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: iconName,
theme: component.theme,
title: itemGroup.title ?? "",
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
} else {
if !itemGroup.items.isEmpty {
if let file = itemGroup.items[0].file {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: true,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: stickerContent.context,
file: file,
animationCache: stickerContent.animationCache,
animationRenderer: stickerContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
}
}
}
contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(stickerContent)))
contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topStickerItems,
activeContentItemIdUpdated: stickersContentItemIdUpdated,
reorderItems: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.reorderPacks(category: .stickers, items: items)
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputStickersIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
))))
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSearchIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
self?.openSearch()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
stickerContent.inputInteraction.openStickerSettings()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent)))
contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent(
theme: component.theme,
items: topStickerItems,
activeContentItemIdUpdated: stickersContentItemIdUpdated,
reorderItems: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.reorderPacks(category: .stickers, items: items)
}
))))
contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputStickersIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
))))
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSearchIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
self?.openSearch()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
component.stickerContent.inputInteraction.openStickerSettings()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent)))
@ -521,10 +530,6 @@ public final class EntityKeyboardComponent: Component {
)
transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize))
if transition.userData(MarkInputCollapsed.self) != nil {
self.searchComponent = nil
}
if let searchComponent = self.searchComponent {
var animateIn = false
let searchView: ComponentHostView<EntitySearchContentEnvironment>
@ -546,7 +551,7 @@ public final class EntityKeyboardComponent: Component {
component: AnyComponent(searchComponent),
environment: {
EntitySearchContentEnvironment(
context: component.stickerContent.context,
context: component.emojiContent.context,
theme: component.theme,
deviceMetrics: component.deviceMetrics
)
@ -669,7 +674,7 @@ public final class EntityKeyboardComponent: Component {
case .emoji:
namespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let _ = (component.stickerContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds)
let _ = (component.emojiContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return

View File

@ -313,7 +313,10 @@ final class EntityKeyboardBottomPanelComponent: Component {
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
}
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0) + 2.0)
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0))
if component.bottomInset > 0.0 {
nextIconOrigin.y += 2.0
}
for icon in panelEnvironment.contentIcons {
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
continue

View File

@ -100,7 +100,6 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
subgroupId: nil
),
context: component.context,
groupId: "topPanel",
attemptSynchronousLoad: false,
file: component.file,
staticEmoji: nil,
@ -110,18 +109,18 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
blurredBadgeColor: .clear,
displayPremiumBadgeIfAvailable: false,
pointSize: CGSize(width: 44.0, height: 44.0),
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder)
strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder, duration: duration)
}
)
self.itemLayer = itemLayer
self.layer.addSublayer(itemLayer)
if itemLayer.displayPlaceholder {
self.updateDisplayPlaceholder(displayPlaceholder: true)
self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0)
}
}
@ -170,7 +169,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
return availableSize
}
private func updateDisplayPlaceholder(displayPlaceholder: Bool) {
private func updateDisplayPlaceholder(displayPlaceholder: Bool, duration: Double) {
if displayPlaceholder {
if self.placeholderView == nil, let component = self.component {
let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView(
@ -188,7 +187,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
} else {
if let placeholderView = self.placeholderView {
self.placeholderView = nil
placeholderView.removeFromSuperview()
if duration > 0.0 {
placeholderView.alpha = 0.0
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak placeholderView] _ in
placeholderView?.removeFromSuperview()
})
} else {
placeholderView.removeFromSuperview()
}
}
}
}

View File

@ -19,10 +19,11 @@ import SoftwareVideo
import AVFoundation
import PhotoResources
import ContextUI
import ShimmerEffect
private class GifVideoLayer: AVSampleBufferDisplayLayer {
private let context: AccountContext
private let file: TelegramMediaFile
private let file: TelegramMediaFile?
private var frameManager: SoftwareVideoLayerFrameManager?
@ -56,7 +57,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
}
}
init(context: AccountContext, file: TelegramMediaFile, synchronousLoad: Bool) {
init(context: AccountContext, file: TelegramMediaFile?, synchronousLoad: Bool) {
self.context = context
self.file = file
@ -64,29 +65,31 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
self.videoGravity = .resizeAspectFill
if let dimensions = file.dimensions {
self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: self.file), synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|> deliverOnMainQueue).start(next: { [weak self] transform in
guard let strongSelf = self else {
return
}
let boundingSize = CGSize(width: 93.0, height: 93.0)
let imageSize = dimensions.cgSize.aspectFilled(boundingSize)
if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.contents = image.cgImage
strongSelf.setupVideo()
strongSelf.started?()
}
if let file = self.file {
if let dimensions = file.dimensions {
self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|> deliverOnMainQueue).start(next: { [weak self] transform in
guard let strongSelf = self else {
return
}
} else {
strongSelf.setupVideo()
}
})
} else {
self.setupVideo()
let boundingSize = CGSize(width: 93.0, height: 93.0)
let imageSize = dimensions.cgSize.aspectFilled(boundingSize)
if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.contents = image.cgImage
strongSelf.setupVideo()
strongSelf.started?()
}
}
} else {
strongSelf.setupVideo()
}
})
} else {
self.setupVideo()
}
}
}
@ -103,7 +106,10 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
}
private func setupVideo() {
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: self.file), layerHolder: nil, layer: self)
guard let file = self.file else {
return
}
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
self.frameManager = frameManager
frameManager.started = { [weak self] in
guard let strongSelf = self else {
@ -127,13 +133,16 @@ public final class GifPagerContentComponent: Component {
public final class InputInteraction {
public let performItemAction: (Item, UIView, CGRect) -> Void
public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
public let loadMore: (String) -> Void
public init(
performItemAction: @escaping (Item, UIView, CGRect) -> Void,
openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void,
loadMore: @escaping (String) -> Void
) {
self.performItemAction = performItemAction
self.openGifContextMenu = openGifContextMenu
self.loadMore = loadMore
}
}
@ -160,17 +169,23 @@ public final class GifPagerContentComponent: Component {
public let inputInteraction: InputInteraction
public let subject: Subject
public let items: [Item]
public let isLoading: Bool
public let loadMoreToken: String?
public init(
context: AccountContext,
inputInteraction: InputInteraction,
subject: Subject,
items: [Item]
items: [Item],
isLoading: Bool,
loadMoreToken: String?
) {
self.context = context
self.inputInteraction = inputInteraction
self.subject = subject
self.items = items
self.isLoading = isLoading
self.loadMoreToken = loadMoreToken
}
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
@ -186,6 +201,12 @@ public final class GifPagerContentComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
if lhs.loadMoreToken != rhs.loadMoreToken {
return false
}
return true
}
@ -256,7 +277,7 @@ public final class GifPagerContentComponent: Component {
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
let maxVisibleIndex = (maxVisibleRow + 1) * self.itemsPerRow - 1
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
@ -266,11 +287,14 @@ public final class GifPagerContentComponent: Component {
}
}
fileprivate enum ItemKey: Hashable {
case media(MediaId)
case placeholder(Int)
}
fileprivate final class ItemLayer: GifVideoLayer {
let item: Item
let item: Item?
private let file: TelegramMediaFile
private let placeholderColor: UIColor
private var disposable: Disposable?
private var fetchDisposable: Disposable?
@ -282,60 +306,32 @@ public final class GifPagerContentComponent: Component {
}
}
}
private var displayPlaceholder: Bool = false
private(set) var displayPlaceholder: Bool = false {
didSet {
if self.displayPlaceholder != oldValue {
self.onUpdateDisplayPlaceholder(self.displayPlaceholder)
}
}
}
let onUpdateDisplayPlaceholder: (Bool) -> Void
init(
item: Item,
item: Item?,
context: AccountContext,
groupId: String,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile,
placeholderColor: UIColor
onUpdateDisplayPlaceholder: @escaping (Bool) -> Void
) {
self.item = item
self.file = file
self.placeholderColor = placeholderColor
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
super.init(context: context, file: file, synchronousLoad: attemptSynchronousLoad)
super.init(context: context, file: item?.file, synchronousLoad: attemptSynchronousLoad)
self.updateDisplayPlaceholder(displayPlaceholder: true)
self.started = { [weak self] in
self?.updateDisplayPlaceholder(displayPlaceholder: false)
}
/*if attemptSynchronousLoad {
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
self.displayPlaceholder = true
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
self.contents = image.cgImage
}
}
}
self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
guard let result = result else {
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
writer.finish()
return
}
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
})
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})*/
}
required init?(coder: NSCoder) {
@ -364,17 +360,36 @@ public final class GifPagerContentComponent: Component {
}
func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.displayPlaceholder == displayPlaceholder {
return
}
self.displayPlaceholder = displayPlaceholder
}
}
final class ItemPlaceholderView: UIView {
private let shimmerView: PortalSourceView?
private var placeholderView: PortalView?
init(shimmerView: PortalSourceView?) {
self.shimmerView = shimmerView
self.placeholderView = PortalView()
if displayPlaceholder {
let placeholderColor = self.placeholderColor
self.backgroundColor = placeholderColor.cgColor
} else {
self.backgroundColor = nil
super.init(frame: CGRect())
self.clipsToBounds = true
if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView {
placeholderView.view.clipsToBounds = true
self.addSubview(placeholderView.view)
shimmerView.addPortal(view: placeholderView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize) {
if let placeholderView = self.placeholderView {
placeholderView.view.frame = CGRect(origin: CGPoint(), size: size)
}
}
}
@ -382,9 +397,13 @@ public final class GifPagerContentComponent: Component {
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
}
private let shimmerHostView: PortalSourceView
private let standaloneShimmerEffect: StandaloneShimmerEffect
private let scrollView: ContentScrollView
private var visibleItemLayers: [MediaId: ItemLayer] = [:]
private var visibleItemPlaceholderViews: [ItemKey: ItemPlaceholderView] = [:]
private var visibleItemLayers: [ItemKey: ItemLayer] = [:]
private var ignoreScrolling: Bool = false
private var component: GifPagerContentComponent?
@ -392,11 +411,19 @@ public final class GifPagerContentComponent: Component {
private var theme: PresentationTheme?
private var itemLayout: ItemLayout?
private var currentLoadMoreToken: String?
override init(frame: CGRect) {
self.shimmerHostView = PortalSourceView()
self.standaloneShimmerEffect = StandaloneShimmerEffect()
self.scrollView = ContentScrollView()
super.init(frame: frame)
self.shimmerHostView.alpha = 0.0
self.addSubview(self.shimmerHostView)
self.scrollView.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
@ -450,7 +477,7 @@ public final class GifPagerContentComponent: Component {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] {
if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[.media(item.file.fileId)] {
component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self))
}
}
@ -473,7 +500,11 @@ public final class GifPagerContentComponent: Component {
for (_, itemLayer) in self.visibleItemLayers {
if itemLayer.frame.contains(localPoint) {
return (itemLayer.item, itemLayer)
if let item = itemLayer.item {
return (item, itemLayer)
} else {
return nil
}
}
}
@ -497,6 +528,13 @@ public final class GifPagerContentComponent: Component {
self.updateVisibleItems(attemptSynchronousLoads: false)
self.updateScrollingOffset(transition: .immediate)
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height - 100.0 {
if let component = self.component, let loadMoreToken = component.loadMoreToken, self.currentLoadMoreToken != loadMoreToken {
self.currentLoadMoreToken = loadMoreToken
component.inputInteraction.loadMore(loadMoreToken)
}
}
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
@ -564,44 +602,104 @@ public final class GifPagerContentComponent: Component {
}
private func updateVisibleItems(attemptSynchronousLoads: Bool) {
guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
var validIds = Set<MediaId>()
var validIds = Set<ItemKey>()
if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) {
for index in itemRange.lowerBound ..< itemRange.upperBound {
let item = component.items[index]
let itemId = item.file.fileId
var item: Item?
let itemId: ItemKey
if index < component.items.count {
item = component.items[index]
itemId = .media(component.items[index].file.fileId)
} else if component.isLoading || component.loadMoreToken != nil {
itemId = .placeholder(index)
} else {
continue
}
validIds.insert(itemId)
let itemFrame = itemLayout.frame(at: index)
let itemTransition: Transition = .immediate
var updateItemLayerPlaceholder = false
let itemLayer: ItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
updateItemLayerPlaceholder = true
itemLayer = ItemLayer(
item: item,
context: component.context,
groupId: "savedGif",
attemptSynchronousLoad: attemptSynchronousLoads,
file: item.file,
placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
guard let strongSelf = self else {
return
}
if displayPlaceholder {
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
let placeholderView: ItemPlaceholderView
if let current = strongSelf.visibleItemPlaceholderViews[itemId] {
placeholderView = current
} else {
placeholderView = ItemPlaceholderView(shimmerView: strongSelf.shimmerHostView)
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
strongSelf.scrollView.insertSubview(placeholderView, at: 0)
}
placeholderView.frame = itemLayer.frame
placeholderView.update(size: placeholderView.bounds.size)
strongSelf.updateShimmerIfNeeded()
}
} else {
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId)
placeholderView.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
}
}
}
)
self.scrollView.layer.addSublayer(itemLayer)
self.visibleItemLayers[itemId] = itemLayer
}
itemLayer.frame = itemLayout.frame(at: index)
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
itemTransition.setFrame(layer: itemLayer, frame: itemFrame)
itemLayer.isVisibleForAnimations = true
if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
itemTransition.setFrame(view: placeholderView, frame: itemFrame)
placeholderView.update(size: itemFrame.size)
}
} else if updateItemLayerPlaceholder {
if itemLayer.displayPlaceholder {
itemLayer.onUpdateDisplayPlaceholder(true)
}
}
}
}
var removedIds: [MediaId] = []
var removedIds: [ItemKey] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
removedIds.append(id)
itemLayer.removeFromSuperlayer()
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
view.removeFromSuperview()
}
}
}
for id in removedIds {
@ -609,13 +707,35 @@ public final class GifPagerContentComponent: Component {
}
}
private func updateShimmerIfNeeded() {
if self.visibleItemPlaceholderViews.isEmpty {
self.standaloneShimmerEffect.layer = nil
} else {
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer
}
}
func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
var contentReset = false
if let previousComponent = self.component, previousComponent.subject != component.subject {
contentReset = true
self.currentLoadMoreToken = nil
}
let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value
self.component = component
self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme
self.theme = keyboardChildEnvironment.theme
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
self.pagerEnvironment = pagerEnvironment
transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize))
let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
let itemLayout = ItemLayout(
width: availableSize.width,
containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right),
@ -631,6 +751,11 @@ public final class GifPagerContentComponent: Component {
if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets {
self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets
}
if contentReset {
self.scrollView.setContentOffset(CGPoint(), animated: false)
}
self.previousScrollingOffset = self.scrollView.contentOffset.y
self.ignoreScrolling = false

View File

@ -297,11 +297,11 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
var targets: [TargetReference] = []
var slotIndex: Int
private let preferredBytesPerRow: Int
private let preferredRowAlignment: Int
init(slotIndex: Int, preferredBytesPerRow: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
init(slotIndex: Int, preferredRowAlignment: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
self.slotIndex = slotIndex
self.preferredBytesPerRow = preferredBytesPerRow
self.preferredRowAlignment = preferredRowAlignment
self.cache = cache
self.stateUpdated = stateUpdated
@ -375,10 +375,10 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
let readyTextureU = texturePoolHalfPlane.take()
let readyTextureV = texturePoolHalfPlane.take()
let readyTextureA = texturePoolFullPlane.take()
let preferredBytesPerRow = self.preferredBytesPerRow
let preferredRowAlignment = self.preferredRowAlignment
return LoadFrameTask(task: { [weak self] in
let frame = item.getFrame(at: timestamp, requestedFormat: .yuva(bytesPerRow: preferredBytesPerRow))
let frame = item.getFrame(at: timestamp, 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)
@ -419,7 +419,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
private let texturePoolFullPlane: TextureStoragePool
private let texturePoolHalfPlane: TextureStoragePool
private let preferredBytesPerRow: Int
private let preferredRowAlignment: Int
private let slotCount: Int
private let slotsX: Int
@ -468,7 +468,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
self.texturePoolFullPlane = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height), format: .r)
self.texturePoolHalfPlane = TextureStoragePool(width: Int(self.cellSize.width) / 2, height: Int(self.cellSize.height) / 2, format: .r)
self.preferredBytesPerRow = alignUp(size: Int(self.cellSize.width), align: TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r))
self.preferredRowAlignment = TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r)
super.init()
@ -508,7 +508,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
for i in 0 ..< self.slotCount {
if self.slotToItemId[i] == nil {
self.slotToItemId[i] = itemId
self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredBytesPerRow: self.preferredBytesPerRow, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredRowAlignment: self.preferredRowAlignment, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
@ -778,7 +778,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
self.isPlaying = isPlaying
}
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
assert(Thread.isMainThread)
let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16)))
@ -805,11 +805,11 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
}
}
public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
return false
}
public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
completion(false)
return EmptyDisposable

View File

@ -6,9 +6,9 @@ import AnimationCache
import Accelerate
public protocol MultiAnimationRenderer: AnyObject {
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable
func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool
func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool
func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
}
private var nextRenderTargetId: Int64 = 1
@ -60,6 +60,9 @@ open class MultiAnimationRenderTarget: SimpleLayer {
open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
}
open func transitionToContents(_ contents: AnyObject) {
}
}
private final class FrameGroup {
@ -178,8 +181,9 @@ private final class ItemAnimationContext {
func updateAddedTarget(target: MultiAnimationRenderTarget) {
if let currentFrameGroup = self.currentFrameGroup {
target.updateDisplayPlaceholder(displayPlaceholder: false)
target.contents = currentFrameGroup.image.cgImage
if let cgImage = currentFrameGroup.image.cgImage {
target.transitionToContents(cgImage)
}
}
self.updateIsPlaying()
@ -239,8 +243,7 @@ private final class ItemAnimationContext {
strongSelf.currentFrameGroup = currentFrameGroup
for target in strongSelf.targets.copyItems() {
if let target = target.value {
target.contents = currentFrameGroup.image.cgImage
target.updateDisplayPlaceholder(displayPlaceholder: false)
target.transitionToContents(currentFrameGroup.image.cgImage!)
}
}
}
@ -400,7 +403,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
private var groupContexts: [String: GroupContext] = [:]
private var groupContext: GroupContext?
private var frameSkip: Int
private var displayLink: ConstantDisplayLinkAnimator?
@ -436,9 +439,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
}
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
if let current = self.groupContext {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
@ -447,7 +450,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
self.groupContext = groupContext
}
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch)
@ -457,9 +460,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
}
public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
if let current = self.groupContext {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
@ -468,15 +471,15 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
self.groupContext = groupContext
}
return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size)
}
public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
if let current = self.groupContext {
groupContext = current
} else {
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
@ -485,7 +488,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
self.groupContext = groupContext
}
return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion)
@ -493,10 +496,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
private func updateIsPlaying() {
var isPlaying = false
for (_, groupContext) in self.groupContexts {
if let groupContext = self.groupContext {
if groupContext.isPlaying {
isPlaying = true
break
}
}
@ -507,7 +509,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
let secondsPerFrame = Double(self.frameSkip) / 60.0
var tasks: [LoadFrameGroupTask] = []
for (_, groupContext) in self.groupContexts {
if let groupContext = self.groupContext {
if groupContext.isPlaying {
tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame))
}

View File

@ -39,6 +39,18 @@ private final class InlineStickerItem: Hashable {
}
}
private final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
public final class TextNodeWithEntities {
public final class Arguments {
public let context: AccountContext
@ -105,6 +117,35 @@ public final class TextNodeWithEntities {
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range)
let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(range.length)
let runDelegateData = RunDelegateData(
ascent: font.ascender,
descent: font.descender,
width: itemSize
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateVersion1,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
string.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: range)
}
}
}
})
@ -168,7 +209,7 @@ public final class TextNodeWithEntities {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
self.inlineStickerItemLayers[id] = itemLayer
self.textNode.layer.addSublayer(itemLayer)
@ -331,7 +372,7 @@ public class ImmediateTextNodeWithEntities: TextNode {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
self.inlineStickerItemLayers[id] = itemLayer
self.layer.addSublayer(itemLayer)

View File

@ -1031,7 +1031,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var insets: UIEdgeInsets
var inputPanelBottomInsetTerm: CGFloat = 0.0
if inputNodeForState != nil {
if let inputNodeForState = inputNodeForState {
if !self.inputPanelContainerNode.stableIsExpanded && inputNodeForState.adjustLayoutForHiddenInput {
inputNodeForState.hideInput = false
inputNodeForState.adjustLayoutForHiddenInput = false
}
insets = layout.insets(options: [])
inputPanelBottomInsetTerm = max(insets.bottom, layout.standardInputHeight)
} else {
@ -1191,9 +1196,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
inputNode.hideInputUpdated = { [weak self] transition in
self?.updateInputPanelBackgroundExpansion(transition: transition)
}
inputNode.expansionFractionUpdated = { [weak self] transition in
self?.updateInputPanelBackgroundExpansion(transition: transition)
}
dismissedInputNode = self.inputNode
if let inputNode = self.inputNode {
@ -1236,7 +1238,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
inputNodeHeightAndOverflow = (
boundedHeight,
max(0.0, inputHeight - boundedHeight)
inputNode.followsDefaultHeight ? max(0.0, inputHeight - boundedHeight) : 0.0
)
} else if let inputNode = self.inputNode {
dismissedInputNode = inputNode
@ -2058,7 +2060,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8)
let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState)
var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState)
if self.chatPresentationInterfaceStateInputView(self.chatPresentationInterfaceState) !== self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) {
updatedInputFocus = true
}
let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState
self.chatPresentationInterfaceState = chatPresentationInterfaceState
@ -2171,18 +2176,22 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated)
}
var waitForKeyboardLayout = false
if let textView = self.textInputPanelNode?.textInputNode?.textView {
let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState)
if textView.inputView !== updatedInputView {
textView.inputView = updatedInputView
if textView.isFirstResponder {
if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) {
waitForKeyboardLayout = true
}
textView.reloadInputViews()
}
}
}
if updatedInputFocus {
if !self.ignoreUpdateHeight {
if !self.ignoreUpdateHeight && !waitForKeyboardLayout {
self.scheduleLayoutTransitionRequest(layoutTransition)
}

View File

@ -238,16 +238,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
},
chatPeerId: chatPeerId
)
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction] item, view, rect in
guard let controllerInteraction = controllerInteraction else {
return
}
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
},
openGifContextMenu: { _, _, _, _, _ in
}
)
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
@ -560,22 +550,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return animatedEmojiStickers
}
// We are intentionally not subscribing to the recent gif updates here
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
for gifItem in savedGifs {
items.append(GifPagerContentComponent.Item(
file: gifItem.contents.get(RecentMediaItem.self)!.media
))
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction] item, view, rect in
guard let controllerInteraction = controllerInteraction else {
return
}
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
},
openGifContextMenu: { _, _, _, _, _ in
},
loadMore: { _ in
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: .recent,
items: items
)
}
)
// We are going to subscribe to the actual data when the view is loaded
let gifItems: Signal<GifPagerContentComponent, NoError> = .single(GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: .recent,
items: [],
isLoading: false,
loadMoreToken: nil
))
return combineLatest(queue: .mainQueue(),
emojiItems,
@ -625,10 +621,195 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
private var gifMode: GifPagerContentComponent.Subject = .recent {
didSet {
self.gifModeSubject.set(self.gifMode)
if self.gifMode != oldValue {
self.reloadGifContext()
}
}
}
private let gifModeSubject: ValuePromise<GifPagerContentComponent.Subject>
private final class GifContext {
private var componentValue: GifPagerContentComponent? {
didSet {
if let componentValue = self.componentValue {
self.componentResult.set(.single(componentValue))
}
}
}
private let componentPromise = Promise<GifPagerContentComponent>()
private let componentResult = Promise<GifPagerContentComponent>()
var component: Signal<GifPagerContentComponent, NoError> {
return self.componentResult.get()
}
private var componentDisposable: Disposable?
private let context: AccountContext
private let subject: GifPagerContentComponent.Subject
private let gifInputInteraction: GifPagerContentComponent.InputInteraction
private var loadingMoreToken: String?
init(context: AccountContext, subject: GifPagerContentComponent.Subject, gifInputInteraction: GifPagerContentComponent.InputInteraction, trendingGifs: Signal<ChatMediaInputGifPaneTrendingState?, NoError>) {
self.context = context
self.subject = subject
self.gifInputInteraction = gifInputInteraction
let gifItems: Signal<GifPagerContentComponent, NoError>
switch subject {
case .recent:
gifItems = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
for gifItem in savedGifs {
items.append(GifPagerContentComponent.Item(
file: gifItem.contents.get(RecentMediaItem.self)!.media
))
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: false,
loadMoreToken: nil
)
}
case .trending:
gifItems = trendingGifs
|> map { trendingGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
var isLoading = false
if let trendingGifs = trendingGifs {
for file in trendingGifs.files {
items.append(GifPagerContentComponent.Item(
file: file.file.media
))
}
} else {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: nil
)
}
case let .emojiSearch(query):
gifItems = paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
var loadMoreToken: String?
var isLoading = false
if let result = result {
for file in result.files {
items.append(GifPagerContentComponent.Item(
file: file.file.media
))
}
loadMoreToken = result.nextOffset
} else {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
)
}
}
self.componentPromise.set(gifItems)
self.componentDisposable = (self.componentPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.componentValue = result
})
}
deinit {
self.componentDisposable?.dispose()
}
func loadMore(token: String) {
if self.loadingMoreToken == token {
return
}
self.loadingMoreToken = token
guard let componentValue = self.componentValue else {
return
}
let context = self.context
let subject = self.subject
let gifInputInteraction = self.gifInputInteraction
switch self.subject {
case let .emojiSearch(query):
let gifItems: Signal<GifPagerContentComponent, NoError>
gifItems = paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in componentValue.items {
items.append(item)
existingIds.insert(item.file.fileId)
}
var loadMoreToken: String?
var isLoading = false
if let result = result {
for file in result.files {
if existingIds.contains(file.file.media.fileId) {
continue
}
existingIds.insert(file.file.media.fileId)
items.append(GifPagerContentComponent.Item(file: file.file.media))
}
if !result.isComplete {
loadMoreToken = result.nextOffset
}
} else {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
)
}
self.componentPromise.set(gifItems)
default:
break
}
}
}
private var gifContext: GifContext? {
didSet {
if let gifContext = self.gifContext {
self.gifComponent.set(gifContext.component)
}
}
}
private let gifComponent = Promise<GifPagerContentComponent>()
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
self.context = context
@ -639,17 +820,18 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.entityKeyboardView = ComponentHostView<Empty>()
self.gifModeSubject = ValuePromise<GifPagerContentComponent.Subject>(self.gifMode, ignoreRepeated: true)
super.init()
self.topBackgroundExtension = 41.0
self.followsDefaultHeight = true
self.view.addSubview(self.entityKeyboardView)
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
updatedInputData,
self.updatedGifs()
self.gifComponent.get()
)
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in
guard let strongSelf = self else {
@ -685,6 +867,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
)
self.trendingGifsPromise.set(.single(nil))
self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { items -> ChatMediaInputGifPaneTrendingState? in
if let items = items {
@ -693,14 +876,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return nil
}
})
}
deinit {
self.inputDataDisposable?.dispose()
}
private func updatedGifs() -> Signal<GifPagerContentComponent, NoError> {
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
self.gifInputInteraction = GifPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction] item, view, rect in
guard let controllerInteraction = controllerInteraction else {
return
@ -712,83 +889,26 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
},
loadMore: { [weak self] token in
guard let strongSelf = self, let gifContext = strongSelf.gifContext else {
return
}
gifContext.loadMore(token: token)
}
)
let context = self.context
let trendingGifs = self.trendingGifsPromise.get()
let updatedGifs = self.gifModeSubject.get()
|> mapToSignal { subject -> Signal<GifPagerContentComponent, NoError> in
switch subject {
case .recent:
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
for gifItem in savedGifs {
items.append(GifPagerContentComponent.Item(
file: gifItem.contents.get(RecentMediaItem.self)!.media
))
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items
)
}
return gifItems
case .trending:
return trendingGifs
|> map { trendingGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
if let trendingGifs = trendingGifs {
for file in trendingGifs.files {
items.append(GifPagerContentComponent.Item(
file: file.file.media
))
}
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items
)
}
case let .emojiSearch(query):
return paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
/*let canLoadMore: Bool
if let result = result {
canLoadMore = !result.isComplete
} else {
canLoadMore = true
}*/
if let result = result {
for file in result.files {
items.append(GifPagerContentComponent.Item(
file: file.file.media
))
}
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items
)
}
}
self.reloadGifContext()
}
deinit {
self.inputDataDisposable?.dispose()
}
private func reloadGifContext() {
if let gifInputInteraction = self.gifInputInteraction {
self.gifContext = GifContext(context: self.context, subject: self.gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
}
return .single(self.currentInputData.gifs)
|> then(updatedGifs)
}
func markInputCollapsed() {
@ -808,7 +928,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let wasMarkedInputCollapsed = self.isMarkInputCollapsed
self.isMarkInputCollapsed = false
let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight)
let expandedHeight = standardInputHeight
var hiddenInputHeight: CGFloat = 0.0
if self.hideInput && !self.adjustLayoutForHiddenInput {
@ -822,18 +942,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
var mappedTransition = Transition(transition)
if wasMarkedInputCollapsed {
if wasMarkedInputCollapsed || !isExpanded {
mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed())
}
var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers
var gifContent: GifPagerContentComponent? = self.currentInputData.gifs
var stickersEnabled = true
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel {
if peer.hasBannedPermission(.banSendStickers) != nil {
stickersEnabled = false
}
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramGroup {
if peer.hasBannedPermission(.banSendStickers) {
stickersEnabled = false
}
}
if !stickersEnabled || interfaceState.interfaceState.editMessage != nil {
stickerContent = nil
gifContent = nil
}
let entityKeyboardSize = self.entityKeyboardView.update(
transition: mappedTransition,
component: AnyComponent(EntityKeyboardComponent(
theme: interfaceState.theme,
bottomInset: bottomInset,
emojiContent: self.currentInputData.emoji,
stickerContent: self.currentInputData.stickers,
gifContent: self.currentInputData.gifs,
stickerContent: stickerContent,
gifContent: gifContent,
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
defaultToEmojiTab: self.defaultToEmojiTab,
externalTopPanelContainer: self.externalTopPanelContainerImpl,
@ -868,7 +1007,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
guard let strongSelf = self else {
return
}
strongSelf.gifModeSubject.set(subject)
strongSelf.gifMode = subject
},
makeSearchContainerNode: { content in
let mappedMode: ChatMediaInputSearchMode

View File

@ -15,15 +15,14 @@ class ChatInputNode: ASDisplayNode {
return nil
}
var topBackgroundExtension: CGFloat = 41.0
var topBackgroundExtension: CGFloat = 0.0
var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
var hideInput: Bool = false
var adjustLayoutForHiddenInput: Bool = false
var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)?
var expansionFraction: CGFloat = 0.0
var expansionFractionUpdated: ((ContainedViewLayoutTransition) -> Void)?
var followsDefaultHeight: Bool = false
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
return (0.0, 0.0)

View File

@ -272,15 +272,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
case .inputButtons:
return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
case .none, .text:
if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage {
let isTextEmpty = editMessage.inputState.inputText.length == 0
let stickersAreEmoji = !isTextEmpty
var stickersEnabled = true
stickersEnabled = true
accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji))
if let _ = chatPresentationInterfaceState.interfaceState.editMessage {
accessoryItems.append(.stickers(isEnabled: true, isEmoji: true))
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
} else {
@ -330,7 +323,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
accessoryItems.append(.commands)
}
accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji))
if stickersEnabled {
accessoryItems.append(.stickers(isEnabled: true, isEmoji: stickersAreEmoji))
} else {
accessoryItems.append(.stickers(isEnabled: true, isEmoji: true))
}
if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id {
accessoryItems.append(.inputButtons)

View File

@ -49,7 +49,7 @@ private final class CachedChatMessageText {
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let textNode: TextNodeWithEntities
private var spoilerTextNode: TextNode?
private var spoilerTextNode: TextNodeWithEntities?
private var dustNode: InvisibleInkDustNode?
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
@ -67,11 +67,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
switch self.visibility {
case .none:
self.textNode.visibilityRect = nil
self.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.textNode.visibilityRect = subRect
self.spoilerTextNode?.visibilityRect = subRect
}
}
}
@ -120,7 +122,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
let statusLayout = self.statusNode.asyncLayout()
let currentCachedChatMessageText = self.cachedChatMessageText
@ -339,9 +341,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor))
let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if !textLayout.spoilers.isEmpty {
spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true))
spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
spoilerTextLayoutAndApply = nil
}
@ -440,33 +442,33 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
let spoilerTextNode = spoilerTextApply()
let spoilerTextNode = spoilerTextApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads))
if strongSelf.spoilerTextNode == nil {
spoilerTextNode.alpha = 0.0
spoilerTextNode.isUserInteractionEnabled = false
spoilerTextNode.contentMode = .topLeft
spoilerTextNode.contentsScale = UIScreenScale
spoilerTextNode.displaysAsynchronously = false
strongSelf.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
spoilerTextNode.textNode.alpha = 0.0
spoilerTextNode.textNode.isUserInteractionEnabled = false
spoilerTextNode.textNode.contentMode = .topLeft
spoilerTextNode.textNode.contentsScale = UIScreenScale
spoilerTextNode.textNode.displaysAsynchronously = false
strongSelf.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
strongSelf.spoilerTextNode = spoilerTextNode
}
strongSelf.spoilerTextNode?.frame = textFrame
strongSelf.spoilerTextNode?.textNode.frame = textFrame
let dustNode: InvisibleInkDustNode
if let current = strongSelf.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode)
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode)
strongSelf.dustNode = dustNode
strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode)
}
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, textColor: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
strongSelf.spoilerTextNode = nil
spoilerTextNode.removeFromSupernode()
spoilerTextNode.textNode.removeFromSupernode()
if let dustNode = strongSelf.dustNode {
strongSelf.dustNode = nil
@ -474,6 +476,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
switch strongSelf.visibility {
case .none:
strongSelf.textNode.visibilityRect = nil
strongSelf.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.textNode.visibilityRect = subRect
strongSelf.spoilerTextNode?.visibilityRect = subRect
}
if let textSelectionNode = strongSelf.textSelectionNode {
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
textSelectionNode.frame = textFrame

View File

@ -56,7 +56,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let lineNode: AnimatedNavigationStripeNode
private let titleNode: AnimatedCountLabelNode
private let textNode: TextNodeWithEntities
private var spoilerTextNode: TextNode?
private var spoilerTextNode: TextNodeWithEntities?
private var dustNode: InvisibleInkDustNode?
private let actionButton: HighlightableButtonNode
private let actionButtonTitleNode: ImmediateTextNode
@ -525,7 +525,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
let makeTitleLayout = self.titleNode.asyncLayout()
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeSpoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
let makeSpoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
let imageNodeLayout = self.imageNode.asyncLayout()
let previousMediaReference = self.previousMediaReference
@ -668,9 +668,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if !textLayout.spoilers.isEmpty {
spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true))
spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
spoilerTextLayoutAndApply = nil
}
@ -701,33 +701,33 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
strongSelf.textNode.textNode.frame = textFrame
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
let spoilerTextNode = spoilerTextApply()
let spoilerTextNode = spoilerTextApply(textArguments)
if strongSelf.spoilerTextNode == nil {
spoilerTextNode.alpha = 0.0
spoilerTextNode.isUserInteractionEnabled = false
spoilerTextNode.contentMode = .topLeft
spoilerTextNode.contentsScale = UIScreenScale
spoilerTextNode.displaysAsynchronously = false
strongSelf.contentTextContainer.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textNode.textNode)
spoilerTextNode.textNode.alpha = 0.0
spoilerTextNode.textNode.isUserInteractionEnabled = false
spoilerTextNode.textNode.contentMode = .topLeft
spoilerTextNode.textNode.contentsScale = UIScreenScale
spoilerTextNode.textNode.displaysAsynchronously = false
strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode)
strongSelf.spoilerTextNode = spoilerTextNode
}
strongSelf.spoilerTextNode?.frame = textFrame
strongSelf.spoilerTextNode?.textNode.frame = textFrame
let dustNode: InvisibleInkDustNode
if let current = strongSelf.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode)
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode)
strongSelf.dustNode = dustNode
strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode)
}
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
strongSelf.spoilerTextNode = nil
spoilerTextNode.removeFromSupernode()
spoilerTextNode.textNode.removeFromSupernode()
if let dustNode = strongSelf.dustNode {
strongSelf.dustNode = nil
@ -735,6 +735,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
}
}
strongSelf.textNode.visibilityRect = CGRect.infinite
strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite
let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight))
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
strongSelf.lineNode.update(

View File

@ -619,10 +619,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.menuButton.cornerRadius = 16.0
self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu
self.menuButtonBackgroundNode = ASDisplayNode()
self.menuButtonBackgroundNode.isUserInteractionEnabled = false
self.menuButtonClippingNode = ASDisplayNode()
self.menuButtonClippingNode.clipsToBounds = true
self.menuButtonClippingNode.isUserInteractionEnabled = false
self.menuButtonIconNode = MenuIconNode()
self.menuButtonIconNode.isUserInteractionEnabled = false
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
self.menuButtonTextNode = ImmediateTextNode()
@ -1782,6 +1785,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
if let textInputNode = self.textInputNode {
textInputNode.textContainerInset = textInputViewRealInsets
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
@ -2069,7 +2073,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let endIndex = currentIndex
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
} else if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
}
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
let textRects = textInputNode.textView.selectionRects(for: textRange)
for textRect in textRects {

View File

@ -126,11 +126,11 @@ public final class TelegramRootController: NavigationController {
}
let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true)
accountSettingsController.tabBarItemDebugTapAction = { [weak self, weak accountSettingsController] in
guard let strongSelf = self, let accountSettingsController = accountSettingsController else {
accountSettingsController.tabBarItemDebugTapAction = { [weak self] in
guard let strongSelf = self else {
return
}
accountSettingsController.push(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
strongSelf.pushViewController(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
}
controllers.append(accountSettingsController)

View File

@ -139,35 +139,6 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
}
})
/*if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider {
let _ = CustomTextAttachmentViewProvider.ensureRegistered
var nextIndex: [String: Int] = [:]
result.string.enumerateSubstrings(in: result.string.startIndex ..< result.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) {
let index: Int
if let value = nextIndex[emoji] {
index = value
} else {
index = 0
}
nextIndex[emoji] = index + 1
let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider)
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
result.replaceCharacters(in: NSRange(substringRange, in: result.string), with: NSAttributedString(attachment: attachment))
stop = true
}
}
}
}*/
return result
}
@ -562,15 +533,6 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
}
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
if let emojiViewProvider = emojiViewProvider {
let _ = emojiViewProvider
/*let emojiText = attributedText.attributedSubstring(from: range)
let attachment = EmojiTextAttachment(index: emojiIndex, text: emojiText.string, emoji: value, viewProvider: emojiViewProvider)
emojiIndex += 1
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
replaceRanges.append((range, attachment))*/
}
}
}
@ -602,52 +564,6 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
textNode.textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment))
}
}
if #available(iOS 15, *), let _ = emojiViewProvider {
let _ = CustomTextAttachmentViewProvider.ensureRegistered
/*var nextIndex: [String: Int] = [:]
var count = 0
let fullRange = NSRange(textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, in: textNode.textView.textStorage.string)
textNode.textView.textStorage.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange, options: [], using: { value, _, _ in
if let _ = value as? EmojiTextAttachment {
count += 1
}
})
while count < 400 {
var found = false
textNode.textView.textStorage.string.enumerateSubstrings(in: textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) {
let index: Int
if let value = nextIndex[emoji] {
index = value
} else {
index = 0
}
nextIndex[emoji] = index + 1
let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider)
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
textNode.textView.textStorage.replaceCharacters(in: NSRange(substringRange, in: textNode.textView.textStorage.string), with: NSAttributedString(attachment: attachment))
count += 1
found = true
stop = true
}
}
}
if !found {
break
}
}*/
}
}
public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) {