mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Emoji input improvements
This commit is contained in:
parent
0b872d86c5
commit
0577baac79
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,8 @@
|
||||
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth];
|
||||
authService.delegate = self;
|
||||
[_authMtProto addMessageService:authService];
|
||||
|
||||
[_authMtProto resume];
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -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];
|
||||
|
@ -96,6 +96,8 @@
|
||||
_requestService.forceBackgroundRequests = true;
|
||||
[_mtProto addMessageService:_requestService];
|
||||
|
||||
[_mtProto resume];
|
||||
|
||||
MTRequest *request = [[MTRequest alloc] init];
|
||||
|
||||
NSData *getConfigData = nil;
|
||||
|
@ -171,9 +171,11 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64;
|
||||
|
||||
_sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context];
|
||||
|
||||
|
||||
|
||||
_shouldStayConnected = true;
|
||||
|
||||
_mtState |= MTProtoStatePaused;
|
||||
|
||||
[self setMtState:_mtState | MTProtoStatePaused];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user