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 {
|
private class CaptionEditableTextNode: EditableTextNode {
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
let previousAlpha = self.alpha
|
let previousAlpha = self.alpha
|
||||||
@ -437,6 +464,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
textInputNode.view.addGestureRecognizer(recognizer)
|
textInputNode.view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
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 {
|
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
||||||
|
@ -656,6 +656,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
|||||||
var textFrame = self.textFieldFrame
|
var textFrame = self.textFieldFrame
|
||||||
textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel)
|
textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel)
|
||||||
textFrame.size.height = self.textInputNode.textView.contentSize.height
|
textFrame.size.height = self.textInputNode.textView.contentSize.height
|
||||||
|
textFrame.size.width -= self.textInputNode.textContainerInset.right
|
||||||
|
|
||||||
if self.textInputNode.isRTL {
|
if self.textInputNode.isRTL {
|
||||||
textFrame.origin.x -= messageOriginDelta
|
textFrame.origin.x -= messageOriginDelta
|
||||||
|
@ -381,9 +381,10 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||||||
if component.contents.contains(where: { $0.id == defaultId }) {
|
if component.contents.contains(where: { $0.id == defaultId }) {
|
||||||
centralId = defaultId
|
centralId = defaultId
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
centralId = component.contents.first?.id
|
|
||||||
}
|
}
|
||||||
|
if centralId == nil {
|
||||||
|
centralId = component.contents.first?.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.centralId != centralId {
|
if self.centralId != centralId {
|
||||||
|
@ -335,6 +335,7 @@ open class BlurredBackgroundView: UIView {
|
|||||||
if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
|
if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
|
||||||
sublayer.backgroundColor = nil
|
sublayer.backgroundColor = nil
|
||||||
sublayer.isOpaque = false
|
sublayer.isOpaque = false
|
||||||
|
//sublayer.setValue(true as NSNumber, forKey: "allowsInPlaceFiltering")
|
||||||
let allowedKeys: [String] = [
|
let allowedKeys: [String] = [
|
||||||
"colorSaturate",
|
"colorSaturate",
|
||||||
"gaussianBlur"
|
"gaussianBlur"
|
||||||
|
@ -147,6 +147,7 @@ public final class TextNodeLayoutArguments {
|
|||||||
public let textShadowColor: UIColor?
|
public let textShadowColor: UIColor?
|
||||||
public let textStroke: (UIColor, CGFloat)?
|
public let textStroke: (UIColor, CGFloat)?
|
||||||
public let displaySpoilers: Bool
|
public let displaySpoilers: Bool
|
||||||
|
public let displayEmbeddedItemsUnderSpoilers: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
attributedString: NSAttributedString?,
|
attributedString: NSAttributedString?,
|
||||||
@ -163,7 +164,8 @@ public final class TextNodeLayoutArguments {
|
|||||||
lineColor: UIColor? = nil,
|
lineColor: UIColor? = nil,
|
||||||
textShadowColor: UIColor? = nil,
|
textShadowColor: UIColor? = nil,
|
||||||
textStroke: (UIColor, CGFloat)? = nil,
|
textStroke: (UIColor, CGFloat)? = nil,
|
||||||
displaySpoilers: Bool = false
|
displaySpoilers: Bool = false,
|
||||||
|
displayEmbeddedItemsUnderSpoilers: Bool = false
|
||||||
) {
|
) {
|
||||||
self.attributedString = attributedString
|
self.attributedString = attributedString
|
||||||
self.backgroundColor = backgroundColor
|
self.backgroundColor = backgroundColor
|
||||||
@ -180,6 +182,7 @@ public final class TextNodeLayoutArguments {
|
|||||||
self.textShadowColor = textShadowColor
|
self.textShadowColor = textShadowColor
|
||||||
self.textStroke = textStroke
|
self.textStroke = textStroke
|
||||||
self.displaySpoilers = displaySpoilers
|
self.displaySpoilers = displaySpoilers
|
||||||
|
self.displayEmbeddedItemsUnderSpoilers = displayEmbeddedItemsUnderSpoilers
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
|
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
|
||||||
@ -198,7 +201,8 @@ public final class TextNodeLayoutArguments {
|
|||||||
lineColor: self.lineColor,
|
lineColor: self.lineColor,
|
||||||
textShadowColor: self.textShadowColor,
|
textShadowColor: self.textShadowColor,
|
||||||
textStroke: self.textStroke,
|
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 {
|
if let attributedString = attributedString {
|
||||||
let stringLength = attributedString.length
|
let stringLength = attributedString.length
|
||||||
|
|
||||||
@ -1126,10 +1130,6 @@ open class TextNode: ASDisplayNode {
|
|||||||
rightOffset = ceil(secondaryRightOffset)
|
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))
|
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
|
isLastLine = true
|
||||||
}
|
}
|
||||||
if isLastLine {
|
if isLastLine {
|
||||||
if attributedString.string.hasPrefix("😀") {
|
|
||||||
assert(true)
|
|
||||||
}
|
|
||||||
if first {
|
if first {
|
||||||
first = false
|
first = false
|
||||||
} else {
|
} else {
|
||||||
@ -1224,12 +1221,6 @@ open class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
|
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] {
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, 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 {
|
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
|
||||||
headIndent = paragraphStyle.headIndent
|
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)
|
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] {
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, 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 {
|
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
|
||||||
headIndent = paragraphStyle.headIndent
|
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)))
|
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
|
||||||
@ -1586,11 +1591,11 @@ open class TextNode: ASDisplayNode {
|
|||||||
if stringMatch {
|
if stringMatch {
|
||||||
layout = existingLayout
|
layout = existingLayout
|
||||||
} else {
|
} 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
|
updated = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2231,11 +2236,11 @@ open class TextView: UIView {
|
|||||||
if stringMatch {
|
if stringMatch {
|
||||||
layout = existingLayout
|
layout = existingLayout
|
||||||
} else {
|
} 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
|
updated = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,8 @@
|
|||||||
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth];
|
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth];
|
||||||
authService.delegate = self;
|
authService.delegate = self;
|
||||||
[_authMtProto addMessageService:authService];
|
[_authMtProto addMessageService:authService];
|
||||||
|
|
||||||
|
[_authMtProto resume];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -91,6 +91,8 @@
|
|||||||
requestService.forceBackgroundRequests = true;
|
requestService.forceBackgroundRequests = true;
|
||||||
[_sourceDatacenterMtProto addMessageService:requestService];
|
[_sourceDatacenterMtProto addMessageService:requestService];
|
||||||
|
|
||||||
|
[_sourceDatacenterMtProto resume];
|
||||||
|
|
||||||
MTRequest *request = [[MTRequest alloc] init];
|
MTRequest *request = [[MTRequest alloc] init];
|
||||||
|
|
||||||
NSData *exportAuthRequestData = nil;
|
NSData *exportAuthRequestData = nil;
|
||||||
@ -130,6 +132,8 @@
|
|||||||
requestService.forceBackgroundRequests = true;
|
requestService.forceBackgroundRequests = true;
|
||||||
[_destinationDatacenterMtProto addMessageService:requestService];
|
[_destinationDatacenterMtProto addMessageService:requestService];
|
||||||
|
|
||||||
|
[_destinationDatacenterMtProto resume];
|
||||||
|
|
||||||
MTRequest *request = [[MTRequest alloc] init];
|
MTRequest *request = [[MTRequest alloc] init];
|
||||||
|
|
||||||
NSData *importAuthRequestData = [_context.serialization importAuthorization:dataId bytes:authData];
|
NSData *importAuthRequestData = [_context.serialization importAuthorization:dataId bytes:authData];
|
||||||
|
@ -96,6 +96,8 @@
|
|||||||
_requestService.forceBackgroundRequests = true;
|
_requestService.forceBackgroundRequests = true;
|
||||||
[_mtProto addMessageService:_requestService];
|
[_mtProto addMessageService:_requestService];
|
||||||
|
|
||||||
|
[_mtProto resume];
|
||||||
|
|
||||||
MTRequest *request = [[MTRequest alloc] init];
|
MTRequest *request = [[MTRequest alloc] init];
|
||||||
|
|
||||||
NSData *getConfigData = nil;
|
NSData *getConfigData = nil;
|
||||||
|
@ -171,9 +171,11 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64;
|
|||||||
|
|
||||||
_sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context];
|
_sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_shouldStayConnected = true;
|
_shouldStayConnected = true;
|
||||||
|
|
||||||
|
_mtState |= MTProtoStatePaused;
|
||||||
|
|
||||||
|
[self setMtState:_mtState | MTProtoStatePaused];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -532,12 +532,25 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef
|
|||||||
|
|
||||||
return thumbnail
|
return thumbnail
|
||||||
|> mapToSignal { thumbnailData in
|
|> mapToSignal { thumbnailData in
|
||||||
return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath)
|
if synchronousLoad, let thumbnailData = thumbnailData {
|
||||||
|> map { fullSize, reducedSize in
|
return .single(Tuple(thumbnailData, nil, false))
|
||||||
if !fullSize._1 && reducedSize._1 {
|
|> then(
|
||||||
return Tuple(thumbnailData, reducedSize._0, false)
|
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
|
var totalSize: UInt64 = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private func printOpenFiles() {
|
public func printOpenFiles() {
|
||||||
var flags: Int32 = 0
|
var flags: Int32 = 0
|
||||||
var fd: Int32 = 0
|
var fd: Int32 = 0
|
||||||
var buf = Data(count: Int(MAXPATHLEN) + 1)
|
var buf = Data(count: Int(MAXPATHLEN) + 1)
|
||||||
|
@ -197,24 +197,23 @@ open class TabBarControllerImpl: ViewController, TabBarController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.selectedIndex == index {
|
let timestamp = CACurrentMediaTime()
|
||||||
let timestamp = CACurrentMediaTime()
|
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
|
||||||
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
|
strongSelf.debugTapCounter.0 = timestamp
|
||||||
strongSelf.debugTapCounter.0 = timestamp
|
strongSelf.debugTapCounter.1 = 0
|
||||||
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 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 {
|
if let validLayout = strongSelf.validLayout {
|
||||||
var updatedLayout = validLayout
|
var updatedLayout = validLayout
|
||||||
|
|
||||||
|
@ -591,6 +591,10 @@ private final class MultipartFetchManager {
|
|||||||
if totalTime > 0.0 {
|
if totalTime > 0.0 {
|
||||||
let speed = Double(totalByteCount) / totalTime
|
let speed = Double(totalByteCount) / totalTime
|
||||||
Logger.shared.log("MultipartFetch", "\(self.resource.id.stringRepresentation) \(speed) bytes/s")
|
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 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 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 */
|
#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);
|
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 final class AnimationCacheItemFrame {
|
||||||
public enum RequestedFormat {
|
public enum RequestedFormat {
|
||||||
case rgba
|
case rgba
|
||||||
case yuva(bytesPerRow: Int)
|
case yuva(rowAlignment: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class Plane {
|
public final class Plane {
|
||||||
@ -50,11 +50,17 @@ public final class AnimationCacheItem {
|
|||||||
public let numFrames: Int
|
public let numFrames: Int
|
||||||
private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?
|
private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?
|
||||||
private let getFrameIndexImpl: (Double) -> Int
|
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.numFrames = numFrames
|
||||||
self.getFrameImpl = getFrame
|
self.getFrameImpl = getFrame
|
||||||
self.getFrameIndexImpl = getFrameIndexImpl
|
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? {
|
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)
|
assert(hashString.count == 32)
|
||||||
var directory = ""
|
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)]))
|
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 {
|
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
|
||||||
@ -209,6 +215,213 @@ private func decompressData(data: Data, range: Range<Int>, decompressedSize: Int
|
|||||||
return decompressedFrameData
|
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 {
|
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||||
struct CompressedResult {
|
struct CompressedResult {
|
||||||
var animationPath: String
|
var animationPath: String
|
||||||
@ -246,7 +459,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
|||||||
private let lock = Lock()
|
private let lock = Lock()
|
||||||
|
|
||||||
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
|
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
|
||||||
self.dctQuality = 67
|
self.dctQuality = 70
|
||||||
|
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.decompressedPath = allocateTempFile()
|
self.decompressedPath = allocateTempFile()
|
||||||
@ -286,7 +499,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
|||||||
} else {
|
} else {
|
||||||
isFirstFrame = true
|
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
|
self.currentSurface = surface
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +512,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
yuvaSurface = ImageYUVA420(width: width, height: height, bytesPerRow: nil)
|
yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil)
|
||||||
self.currentYUVASurface = yuvaSurface
|
self.currentYUVASurface = yuvaSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,17 +769,17 @@ private final class AnimationCacheItemAccessor {
|
|||||||
if let currentYUVASurface = self.currentYUVASurface {
|
if let currentYUVASurface = self.currentYUVASurface {
|
||||||
yuvaSurface = currentYUVASurface
|
yuvaSurface = currentYUVASurface
|
||||||
} else {
|
} 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):
|
case let .yuva(preferredRowAlignment):
|
||||||
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: preferredBytesPerRow)
|
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface)
|
self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface)
|
||||||
|
|
||||||
switch requestedFormat {
|
switch requestedFormat {
|
||||||
case .rgba:
|
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)
|
yuvaSurface.toARGB(target: currentSurface)
|
||||||
self.currentYUVASurface = yuvaSurface
|
self.currentYUVASurface = yuvaSurface
|
||||||
|
|
||||||
@ -619,6 +832,14 @@ private final class AnimationCacheItemAccessor {
|
|||||||
}
|
}
|
||||||
return self.durationMapping.count - 1
|
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 {
|
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)
|
return itemAccessor.getFrame(index: index, requestedFormat: requestedFormat)
|
||||||
}, getFrameIndexImpl: { duration in
|
}, getFrameIndexImpl: { duration in
|
||||||
return itemAccessor.getFrameIndex(duration: duration)
|
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 {
|
public final class AnimationCacheImpl: AnimationCache {
|
||||||
private final class Impl {
|
private final class Impl {
|
||||||
private final class ItemContext {
|
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 {
|
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 itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||||
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
|
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
|
||||||
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
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? {
|
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
|
||||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
|
let hashString = md5Hash(sourceId)
|
||||||
|
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
|
||||||
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
||||||
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
|
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
|
||||||
return loadItem(path: 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 {
|
static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
|
||||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
|
let hashString = md5Hash(sourceId)
|
||||||
|
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
|
||||||
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
||||||
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) {
|
if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) {
|
||||||
completion(item)
|
completion(item)
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
} else {
|
|
||||||
completion(nil)
|
|
||||||
|
|
||||||
return EmptyDisposable
|
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 queue: Queue
|
||||||
private let basePath: String
|
private let basePath: String
|
||||||
private let impl: QueueLocalObject<Impl>
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
private let allocateTempFile: () -> String
|
||||||
|
|
||||||
public init(basePath: String, allocateTempFile: @escaping () -> String) {
|
public init(basePath: String, allocateTempFile: @escaping () -> String) {
|
||||||
let queue = Queue()
|
let queue = Queue()
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.basePath = basePath
|
self.basePath = basePath
|
||||||
|
self.allocateTempFile = allocateTempFile
|
||||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||||
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
|
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? {
|
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 {
|
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
let basePath = self.basePath
|
let basePath = self.basePath
|
||||||
|
let allocateTempFile = self.allocateTempFile
|
||||||
queue.async {
|
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
|
return disposable
|
||||||
|
@ -2,27 +2,46 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import ImageDCT
|
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 {
|
final class ImagePlane {
|
||||||
let width: Int
|
let width: Int
|
||||||
let height: Int
|
let height: Int
|
||||||
let bytesPerRow: Int
|
let bytesPerRow: Int
|
||||||
|
let rowAlignment: Int
|
||||||
let components: Int
|
let components: Int
|
||||||
var data: Data
|
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.width = width
|
||||||
self.height = height
|
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.components = components
|
||||||
self.data = Data(count: self.bytesPerRow * height)
|
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 {
|
final class ImageARGB {
|
||||||
let argbPlane: ImagePlane
|
let argbPlane: ImagePlane
|
||||||
|
|
||||||
init(width: Int, height: Int, bytesPerRow: Int?) {
|
init(width: Int, height: Int, rowAlignment: Int?) {
|
||||||
self.argbPlane = ImagePlane(width: width, height: height, components: 4, bytesPerRow: bytesPerRow)
|
self.argbPlane = ImagePlane(width: width, height: height, components: 4, rowAlignment: rowAlignment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,11 +51,11 @@ final class ImageYUVA420 {
|
|||||||
let vPlane: ImagePlane
|
let vPlane: ImagePlane
|
||||||
let aPlane: ImagePlane
|
let aPlane: ImagePlane
|
||||||
|
|
||||||
init(width: Int, height: Int, bytesPerRow: Int?) {
|
init(width: Int, height: Int, rowAlignment: Int?) {
|
||||||
self.yPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
|
self.yPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
|
||||||
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
|
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
|
||||||
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
|
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
|
||||||
self.aPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
|
self.aPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,8 +111,8 @@ extension ImageARGB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toYUVA420(bytesPerRow: Int?) -> ImageYUVA420 {
|
func toYUVA420(rowAlignment: Int?) -> ImageYUVA420 {
|
||||||
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, bytesPerRow: bytesPerRow)
|
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, rowAlignment: rowAlignment)
|
||||||
self.toYUVA420(target: resultImage)
|
self.toYUVA420(target: resultImage)
|
||||||
return resultImage
|
return resultImage
|
||||||
}
|
}
|
||||||
@ -125,8 +144,8 @@ extension ImageYUVA420 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toARGB(bytesPerRow: Int?) -> ImageARGB {
|
func toARGB(rowAlignment: Int?) -> ImageARGB {
|
||||||
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
|
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
|
||||||
self.toARGB(target: resultImage)
|
self.toARGB(target: resultImage)
|
||||||
return resultImage
|
return resultImage
|
||||||
}
|
}
|
||||||
@ -221,8 +240,8 @@ extension DctCoefficientsYUVA420 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func idct(dctData: DctData, bytesPerRow: Int?) -> ImageYUVA420 {
|
func idct(dctData: DctData, rowAlignment: Int?) -> ImageYUVA420 {
|
||||||
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
|
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
|
||||||
self.idct(dctData: dctData, target: resultImage)
|
self.idct(dctData: dctData, target: resultImage)
|
||||||
return resultImage
|
return resultImage
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let groupId: String
|
|
||||||
private let emoji: ChatTextInputTextCustomEmojiAttribute
|
private let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||||
private let cache: AnimationCache
|
private let cache: AnimationCache
|
||||||
private let renderer: MultiAnimationRenderer
|
private let renderer: MultiAnimationRenderer
|
||||||
@ -39,6 +38,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
private let pointSize: CGSize
|
private let pointSize: CGSize
|
||||||
private let pixelSize: CGSize
|
private let pixelSize: CGSize
|
||||||
|
|
||||||
|
private var isDisplayingPlaceholder: Bool = false
|
||||||
|
|
||||||
private var file: TelegramMediaFile?
|
private var file: TelegramMediaFile?
|
||||||
private var infoDisposable: Disposable?
|
private var infoDisposable: Disposable?
|
||||||
private var disposable: 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.context = context
|
||||||
self.groupId = groupId
|
|
||||||
self.emoji = emoji
|
self.emoji = emoji
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.renderer = renderer
|
self.renderer = renderer
|
||||||
@ -123,9 +123,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
self.file = file
|
self.file = file
|
||||||
|
|
||||||
if attemptSynchronousLoad {
|
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) {
|
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.contents = image.cgImage
|
||||||
|
self.isDisplayingPlaceholder = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,10 +134,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
} else {
|
} else {
|
||||||
let pointSize = self.pointSize
|
let pointSize = self.pointSize
|
||||||
let placeholderColor = self.placeholderColor
|
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 {
|
if !result {
|
||||||
MultiAnimationRendererImpl.firstFrameQueue.async {
|
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 {
|
DispatchQueue.main.async {
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -144,6 +145,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
}
|
}
|
||||||
if let image = image {
|
if let image = image {
|
||||||
strongSelf.contents = image.cgImage
|
strongSelf.contents = image.cgImage
|
||||||
|
strongSelf.isDisplayingPlaceholder = true
|
||||||
}
|
}
|
||||||
strongSelf.loadAnimation()
|
strongSelf.loadAnimation()
|
||||||
}
|
}
|
||||||
@ -165,7 +167,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
|
|
||||||
let context = self.context
|
let context = self.context
|
||||||
if file.isAnimatedSticker || file.isVideoEmoji {
|
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 source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||||
|
|
||||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||||
@ -192,7 +194,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} 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
|
let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in
|
||||||
guard result.complete else {
|
guard result.complete else {
|
||||||
return
|
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 {
|
public final class EmojiTextAttachmentView: UIView {
|
||||||
private let contentLayer: InlineStickerItemLayer
|
private let contentLayer: InlineStickerItemLayer
|
||||||
|
|
||||||
public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
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())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
@ -537,12 +537,11 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private(set) var displayPlaceholder: Bool = false
|
private(set) var displayPlaceholder: Bool = false
|
||||||
let onUpdateDisplayPlaceholder: (Bool) -> Void
|
let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
item: Item,
|
item: Item,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
groupId: String,
|
|
||||||
attemptSynchronousLoad: Bool,
|
attemptSynchronousLoad: Bool,
|
||||||
file: TelegramMediaFile?,
|
file: TelegramMediaFile?,
|
||||||
staticEmoji: String?,
|
staticEmoji: String?,
|
||||||
@ -552,7 +551,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
blurredBadgeColor: UIColor,
|
blurredBadgeColor: UIColor,
|
||||||
displayPremiumBadgeIfAvailable: Bool,
|
displayPremiumBadgeIfAvailable: Bool,
|
||||||
pointSize: CGSize,
|
pointSize: CGSize,
|
||||||
onUpdateDisplayPlaceholder: @escaping (Bool) -> Void
|
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
||||||
) {
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.file = file
|
self.file = file
|
||||||
@ -573,7 +572,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
return
|
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 source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||||
|
|
||||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||||
@ -604,13 +603,13 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attemptSynchronousLoad {
|
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)
|
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAnimation()
|
loadAnimation()
|
||||||
} else {
|
} 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()
|
loadAnimation()
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
@ -682,7 +681,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.placeholderColor = layer.placeholderColor
|
self.placeholderColor = layer.placeholderColor
|
||||||
self.size = layer.size
|
self.size = layer.size
|
||||||
|
|
||||||
self.onUpdateDisplayPlaceholder = { _ in }
|
self.onUpdateDisplayPlaceholder = { _, _ in }
|
||||||
|
|
||||||
super.init(layer: layer)
|
super.init(layer: layer)
|
||||||
}
|
}
|
||||||
@ -718,47 +717,17 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.displayPlaceholder = displayPlaceholder
|
self.displayPlaceholder = displayPlaceholder
|
||||||
self.onUpdateDisplayPlaceholder(displayPlaceholder)
|
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
/*if displayPlaceholder {
|
override func transitionToContents(_ contents: AnyObject) {
|
||||||
if self.placeholderView == nil {
|
self.contents = contents
|
||||||
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 self.displayPlaceholder {
|
||||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: .black) {
|
self.displayPlaceholder = false
|
||||||
Queue.mainQueue().async {
|
self.onUpdateDisplayPlaceholder(false, 0.2)
|
||||||
guard let strongSelf = self else {
|
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||||
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
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -790,6 +759,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
private let boundsChangeTrackerLayer = SimpleLayer()
|
private let boundsChangeTrackerLayer = SimpleLayer()
|
||||||
private var effectiveVisibleSize: CGSize = CGSize()
|
private var effectiveVisibleSize: CGSize = CGSize()
|
||||||
|
|
||||||
|
private let placeholdersContainerView: UIView
|
||||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||||
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
||||||
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
|
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
|
||||||
@ -812,12 +782,13 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.shimmerHostView = PortalSourceView()
|
self.shimmerHostView = PortalSourceView()
|
||||||
|
|
||||||
self.standaloneShimmerEffect = StandaloneShimmerEffect()
|
self.standaloneShimmerEffect = StandaloneShimmerEffect()
|
||||||
|
|
||||||
self.scrollView = ContentScrollView()
|
self.scrollView = ContentScrollView()
|
||||||
self.scrollView.layer.anchorPoint = CGPoint()
|
self.scrollView.layer.anchorPoint = CGPoint()
|
||||||
|
|
||||||
|
self.placeholdersContainerView = UIView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.shimmerHostView.alpha = 0.0
|
self.shimmerHostView.alpha = 0.0
|
||||||
@ -842,6 +813,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.scrollView.clipsToBounds = false
|
self.scrollView.clipsToBounds = false
|
||||||
self.addSubview(self.scrollView)
|
self.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.scrollView.addSubview(self.placeholdersContainerView)
|
||||||
|
|
||||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
|
||||||
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||||
@ -1404,7 +1377,6 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
itemLayer = ItemLayer(
|
itemLayer = ItemLayer(
|
||||||
item: item,
|
item: item,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
groupId: "keyboard-\(Int(itemLayout.nativeItemSize))",
|
|
||||||
attemptSynchronousLoad: attemptSynchronousLoads,
|
attemptSynchronousLoad: attemptSynchronousLoads,
|
||||||
file: item.file,
|
file: item.file,
|
||||||
staticEmoji: item.staticEmoji,
|
staticEmoji: item.staticEmoji,
|
||||||
@ -1414,7 +1386,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
|
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
|
||||||
displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges,
|
displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges,
|
||||||
pointSize: itemNativeFitSize,
|
pointSize: itemNativeFitSize,
|
||||||
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
|
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1432,7 +1404,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
size: itemNativeFitSize
|
size: itemNativeFitSize
|
||||||
)
|
)
|
||||||
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
|
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
|
||||||
strongSelf.scrollView.insertSubview(placeholderView, at: 0)
|
strongSelf.placeholdersContainerView.addSubview(placeholderView)
|
||||||
}
|
}
|
||||||
placeholderView.frame = itemLayer.frame
|
placeholderView.frame = itemLayer.frame
|
||||||
placeholderView.update(size: placeholderView.bounds.size)
|
placeholderView.update(size: placeholderView.bounds.size)
|
||||||
@ -1442,9 +1414,20 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
|
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
|
||||||
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: 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 {
|
} else if updateItemLayerPlaceholder {
|
||||||
if itemLayer.displayPlaceholder {
|
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] = []
|
var removedIds: [ItemLayer.Key] = []
|
||||||
for (id, itemLayer) in self.visibleItemLayers {
|
for (id, itemLayer) in self.visibleItemLayers {
|
||||||
if !validIds.contains(id) {
|
if !validIds.contains(id) {
|
||||||
@ -1491,6 +1475,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
||||||
view.removeFromSuperview()
|
view.removeFromSuperview()
|
||||||
|
removedPlaceholerViews = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1527,13 +1512,17 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.visibleGroupPremiumButtons.removeValue(forKey: id)
|
self.visibleGroupPremiumButtons.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if removedPlaceholerViews {
|
||||||
|
self.updateShimmerIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
if let topVisibleGroupId = topVisibleGroupId {
|
if let topVisibleGroupId = topVisibleGroupId {
|
||||||
self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate))
|
self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateShimmerIfNeeded() {
|
private func updateShimmerIfNeeded() {
|
||||||
if self.visibleItemPlaceholderViews.isEmpty {
|
if self.placeholdersContainerView.subviews.isEmpty {
|
||||||
self.standaloneShimmerEffect.layer = nil
|
self.standaloneShimmerEffect.layer = nil
|
||||||
} else {
|
} else {
|
||||||
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer
|
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer
|
||||||
|
@ -76,8 +76,8 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
public let theme: PresentationTheme
|
public let theme: PresentationTheme
|
||||||
public let bottomInset: CGFloat
|
public let bottomInset: CGFloat
|
||||||
public let emojiContent: EmojiPagerContentComponent
|
public let emojiContent: EmojiPagerContentComponent
|
||||||
public let stickerContent: EmojiPagerContentComponent
|
public let stickerContent: EmojiPagerContentComponent?
|
||||||
public let gifContent: GifPagerContentComponent
|
public let gifContent: GifPagerContentComponent?
|
||||||
public let availableGifSearchEmojies: [GifSearchEmoji]
|
public let availableGifSearchEmojies: [GifSearchEmoji]
|
||||||
public let defaultToEmojiTab: Bool
|
public let defaultToEmojiTab: Bool
|
||||||
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
||||||
@ -94,8 +94,8 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
bottomInset: CGFloat,
|
bottomInset: CGFloat,
|
||||||
emojiContent: EmojiPagerContentComponent,
|
emojiContent: EmojiPagerContentComponent,
|
||||||
stickerContent: EmojiPagerContentComponent,
|
stickerContent: EmojiPagerContentComponent?,
|
||||||
gifContent: GifPagerContentComponent,
|
gifContent: GifPagerContentComponent?,
|
||||||
availableGifSearchEmojies: [GifSearchEmoji],
|
availableGifSearchEmojies: [GifSearchEmoji],
|
||||||
defaultToEmojiTab: Bool,
|
defaultToEmojiTab: Bool,
|
||||||
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
||||||
@ -201,170 +201,179 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
|
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
|
||||||
|
|
||||||
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
||||||
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent)))
|
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
||||||
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
|
|
||||||
//TODO:localize
|
if transition.userData(MarkInputCollapsed.self) != nil {
|
||||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
self.searchComponent = nil
|
||||||
id: "recent",
|
}
|
||||||
isReorderable: false,
|
|
||||||
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
if let gifContent = component.gifContent {
|
||||||
imageName: "Chat/Input/Media/RecentTabIcon",
|
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent)))
|
||||||
theme: component.theme,
|
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||||
title: "Recent",
|
//TODO:localize
|
||||||
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 {
|
|
||||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||||
id: emoji.emoji,
|
id: "recent",
|
||||||
isReorderable: false,
|
isReorderable: false,
|
||||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||||
context: component.stickerContent.context,
|
imageName: "Chat/Input/Media/RecentTabIcon",
|
||||||
file: emoji.file,
|
|
||||||
animationCache: component.stickerContent.animationCache,
|
|
||||||
animationRenderer: component.stickerContent.animationRenderer,
|
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
title: emoji.title,
|
title: "Recent",
|
||||||
pressed: { [weak self] in
|
pressed: { [weak self] in
|
||||||
self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji))
|
self?.component?.switchToGifSubject(.recent)
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
))
|
))
|
||||||
}
|
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||||
let defaultActiveGifItemId: AnyHashable
|
id: "trending",
|
||||||
switch component.gifContent.subject {
|
isReorderable: false,
|
||||||
case .recent:
|
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||||
defaultActiveGifItemId = "recent"
|
imageName: "Chat/Input/Media/TrendingGifs",
|
||||||
case .trending:
|
theme: component.theme,
|
||||||
defaultActiveGifItemId = "trending"
|
title: "Trending",
|
||||||
case let .emojiSearch(value):
|
pressed: { [weak self] in
|
||||||
defaultActiveGifItemId = AnyHashable(value)
|
self?.component?.switchToGifSubject(.trending)
|
||||||
}
|
}
|
||||||
contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent(
|
))
|
||||||
theme: component.theme,
|
))
|
||||||
items: topGifItems,
|
for emoji in component.availableGifSearchEmojies {
|
||||||
defaultActiveItemId: defaultActiveGifItemId,
|
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||||
activeContentItemIdUpdated: gifsContentItemIdUpdated,
|
id: emoji.emoji,
|
||||||
reorderItems: { _ in
|
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))
|
||||||
|
}
|
||||||
|
))
|
||||||
|
))
|
||||||
}
|
}
|
||||||
))))
|
let defaultActiveGifItemId: AnyHashable
|
||||||
contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent(
|
switch gifContent.subject {
|
||||||
name: "Chat/Input/Media/EntityInputGifsIcon",
|
case .recent:
|
||||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
defaultActiveGifItemId = "recent"
|
||||||
maxSize: nil
|
case .trending:
|
||||||
))))
|
defaultActiveGifItemId = "trending"
|
||||||
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
|
case let .emojiSearch(value):
|
||||||
content: AnyComponent(BundleIconComponent(
|
defaultActiveGifItemId = AnyHashable(value)
|
||||||
name: "Chat/Input/Media/EntityInputSearchIcon",
|
}
|
||||||
|
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,
|
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||||
maxSize: nil
|
maxSize: nil
|
||||||
)),
|
))))
|
||||||
action: { [weak self] in
|
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
|
||||||
self?.openSearch()
|
content: AnyComponent(BundleIconComponent(
|
||||||
}
|
name: "Chat/Input/Media/EntityInputSearchIcon",
|
||||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||||
|
maxSize: nil
|
||||||
var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = []
|
)),
|
||||||
for itemGroup in component.stickerContent.itemGroups {
|
action: { [weak self] in
|
||||||
if let id = itemGroup.supergroupId.base as? String {
|
self?.openSearch()
|
||||||
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)
|
|
||||||
}
|
|
||||||
))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
} else {
|
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||||
if !itemGroup.items.isEmpty {
|
}
|
||||||
if let file = itemGroup.items[0].file {
|
|
||||||
|
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(
|
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||||
id: itemGroup.supergroupId,
|
id: itemGroup.supergroupId,
|
||||||
isReorderable: true,
|
isReorderable: false,
|
||||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||||
context: component.stickerContent.context,
|
imageName: iconName,
|
||||||
file: file,
|
|
||||||
animationCache: component.stickerContent.animationCache,
|
|
||||||
animationRenderer: component.stickerContent.animationRenderer,
|
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
title: itemGroup.title ?? "",
|
title: title,
|
||||||
pressed: { [weak self] in
|
pressed: { [weak self] in
|
||||||
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
|
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)>()
|
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
||||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent)))
|
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))
|
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 {
|
if let searchComponent = self.searchComponent {
|
||||||
var animateIn = false
|
var animateIn = false
|
||||||
let searchView: ComponentHostView<EntitySearchContentEnvironment>
|
let searchView: ComponentHostView<EntitySearchContentEnvironment>
|
||||||
@ -546,7 +551,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
component: AnyComponent(searchComponent),
|
component: AnyComponent(searchComponent),
|
||||||
environment: {
|
environment: {
|
||||||
EntitySearchContentEnvironment(
|
EntitySearchContentEnvironment(
|
||||||
context: component.stickerContent.context,
|
context: component.emojiContent.context,
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
deviceMetrics: component.deviceMetrics
|
deviceMetrics: component.deviceMetrics
|
||||||
)
|
)
|
||||||
@ -669,7 +674,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
case .emoji:
|
case .emoji:
|
||||||
namespace = Namespaces.ItemCollection.CloudEmojiPacks
|
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
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
@ -313,7 +313,10 @@ final class EntityKeyboardBottomPanelComponent: Component {
|
|||||||
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
|
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 {
|
for icon in panelEnvironment.contentIcons {
|
||||||
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
|
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
|
||||||
continue
|
continue
|
||||||
|
@ -100,7 +100,6 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
|||||||
subgroupId: nil
|
subgroupId: nil
|
||||||
),
|
),
|
||||||
context: component.context,
|
context: component.context,
|
||||||
groupId: "topPanel",
|
|
||||||
attemptSynchronousLoad: false,
|
attemptSynchronousLoad: false,
|
||||||
file: component.file,
|
file: component.file,
|
||||||
staticEmoji: nil,
|
staticEmoji: nil,
|
||||||
@ -110,18 +109,18 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
|||||||
blurredBadgeColor: .clear,
|
blurredBadgeColor: .clear,
|
||||||
displayPremiumBadgeIfAvailable: false,
|
displayPremiumBadgeIfAvailable: false,
|
||||||
pointSize: CGSize(width: 44.0, height: 44.0),
|
pointSize: CGSize(width: 44.0, height: 44.0),
|
||||||
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
|
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder)
|
strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder, duration: duration)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.itemLayer = itemLayer
|
self.itemLayer = itemLayer
|
||||||
self.layer.addSublayer(itemLayer)
|
self.layer.addSublayer(itemLayer)
|
||||||
|
|
||||||
if itemLayer.displayPlaceholder {
|
if itemLayer.displayPlaceholder {
|
||||||
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +169,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
|||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
private func updateDisplayPlaceholder(displayPlaceholder: Bool, duration: Double) {
|
||||||
if displayPlaceholder {
|
if displayPlaceholder {
|
||||||
if self.placeholderView == nil, let component = self.component {
|
if self.placeholderView == nil, let component = self.component {
|
||||||
let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView(
|
let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView(
|
||||||
@ -188,7 +187,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
if let placeholderView = self.placeholderView {
|
if let placeholderView = self.placeholderView {
|
||||||
self.placeholderView = nil
|
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 AVFoundation
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import ContextUI
|
import ContextUI
|
||||||
|
import ShimmerEffect
|
||||||
|
|
||||||
private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let file: TelegramMediaFile
|
private let file: TelegramMediaFile?
|
||||||
|
|
||||||
private var frameManager: SoftwareVideoLayerFrameManager?
|
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.context = context
|
||||||
self.file = file
|
self.file = file
|
||||||
|
|
||||||
@ -64,29 +65,31 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
|||||||
|
|
||||||
self.videoGravity = .resizeAspectFill
|
self.videoGravity = .resizeAspectFill
|
||||||
|
|
||||||
if let dimensions = file.dimensions {
|
if let file = self.file {
|
||||||
self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: self.file), synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|
if let dimensions = file.dimensions {
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|
||||||
guard let strongSelf = self else {
|
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
||||||
return
|
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?()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
let boundingSize = CGSize(width: 93.0, height: 93.0)
|
||||||
strongSelf.setupVideo()
|
let imageSize = dimensions.cgSize.aspectFilled(boundingSize)
|
||||||
}
|
|
||||||
})
|
if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() {
|
||||||
} else {
|
Queue.mainQueue().async {
|
||||||
self.setupVideo()
|
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() {
|
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
|
self.frameManager = frameManager
|
||||||
frameManager.started = { [weak self] in
|
frameManager.started = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -127,13 +133,16 @@ public final class GifPagerContentComponent: Component {
|
|||||||
public final class InputInteraction {
|
public final class InputInteraction {
|
||||||
public let performItemAction: (Item, UIView, CGRect) -> Void
|
public let performItemAction: (Item, UIView, CGRect) -> Void
|
||||||
public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
|
public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void
|
||||||
|
public let loadMore: (String) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
performItemAction: @escaping (Item, UIView, CGRect) -> Void,
|
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.performItemAction = performItemAction
|
||||||
self.openGifContextMenu = openGifContextMenu
|
self.openGifContextMenu = openGifContextMenu
|
||||||
|
self.loadMore = loadMore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,17 +169,23 @@ public final class GifPagerContentComponent: Component {
|
|||||||
public let inputInteraction: InputInteraction
|
public let inputInteraction: InputInteraction
|
||||||
public let subject: Subject
|
public let subject: Subject
|
||||||
public let items: [Item]
|
public let items: [Item]
|
||||||
|
public let isLoading: Bool
|
||||||
|
public let loadMoreToken: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
inputInteraction: InputInteraction,
|
inputInteraction: InputInteraction,
|
||||||
subject: Subject,
|
subject: Subject,
|
||||||
items: [Item]
|
items: [Item],
|
||||||
|
isLoading: Bool,
|
||||||
|
loadMoreToken: String?
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.inputInteraction = inputInteraction
|
self.inputInteraction = inputInteraction
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.isLoading = isLoading
|
||||||
|
self.loadMoreToken = loadMoreToken
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
|
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
|
||||||
@ -186,6 +201,12 @@ public final class GifPagerContentComponent: Component {
|
|||||||
if lhs.items != rhs.items {
|
if lhs.items != rhs.items {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isLoading != rhs.isLoading {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.loadMoreToken != rhs.loadMoreToken {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -256,7 +277,7 @@ public final class GifPagerContentComponent: Component {
|
|||||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
||||||
|
|
||||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
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 {
|
if maxVisibleIndex >= minVisibleIndex {
|
||||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||||
@ -266,11 +287,14 @@ public final class GifPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate final class ItemLayer: GifVideoLayer {
|
fileprivate enum ItemKey: Hashable {
|
||||||
let item: Item
|
case media(MediaId)
|
||||||
|
case placeholder(Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate final class ItemLayer: GifVideoLayer {
|
||||||
|
let item: Item?
|
||||||
|
|
||||||
private let file: TelegramMediaFile
|
|
||||||
private let placeholderColor: UIColor
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
private var fetchDisposable: 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(
|
init(
|
||||||
item: Item,
|
item: Item?,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
groupId: String,
|
groupId: String,
|
||||||
attemptSynchronousLoad: Bool,
|
attemptSynchronousLoad: Bool,
|
||||||
file: TelegramMediaFile,
|
onUpdateDisplayPlaceholder: @escaping (Bool) -> Void
|
||||||
placeholderColor: UIColor
|
|
||||||
) {
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.file = file
|
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||||
self.placeholderColor = placeholderColor
|
|
||||||
|
|
||||||
super.init(context: context, file: file, synchronousLoad: attemptSynchronousLoad)
|
super.init(context: context, file: item?.file, synchronousLoad: attemptSynchronousLoad)
|
||||||
|
|
||||||
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||||
|
|
||||||
self.started = { [weak self] in
|
self.started = { [weak self] in
|
||||||
self?.updateDisplayPlaceholder(displayPlaceholder: false)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
@ -364,17 +360,36 @@ public final class GifPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
||||||
if self.displayPlaceholder == displayPlaceholder {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.displayPlaceholder = displayPlaceholder
|
self.displayPlaceholder = displayPlaceholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if displayPlaceholder {
|
final class ItemPlaceholderView: UIView {
|
||||||
let placeholderColor = self.placeholderColor
|
private let shimmerView: PortalSourceView?
|
||||||
self.backgroundColor = placeholderColor.cgColor
|
private var placeholderView: PortalView?
|
||||||
} else {
|
|
||||||
self.backgroundColor = nil
|
init(shimmerView: PortalSourceView?) {
|
||||||
|
self.shimmerView = shimmerView
|
||||||
|
self.placeholderView = PortalView()
|
||||||
|
|
||||||
|
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 final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let shimmerHostView: PortalSourceView
|
||||||
|
private let standaloneShimmerEffect: StandaloneShimmerEffect
|
||||||
|
|
||||||
private let scrollView: ContentScrollView
|
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 ignoreScrolling: Bool = false
|
||||||
|
|
||||||
private var component: GifPagerContentComponent?
|
private var component: GifPagerContentComponent?
|
||||||
@ -392,11 +411,19 @@ public final class GifPagerContentComponent: Component {
|
|||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
private var itemLayout: ItemLayout?
|
private var itemLayout: ItemLayout?
|
||||||
|
|
||||||
|
private var currentLoadMoreToken: String?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
self.shimmerHostView = PortalSourceView()
|
||||||
|
self.standaloneShimmerEffect = StandaloneShimmerEffect()
|
||||||
|
|
||||||
self.scrollView = ContentScrollView()
|
self.scrollView = ContentScrollView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.shimmerHostView.alpha = 0.0
|
||||||
|
self.addSubview(self.shimmerHostView)
|
||||||
|
|
||||||
self.scrollView.delaysContentTouches = false
|
self.scrollView.delaysContentTouches = false
|
||||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
@ -450,7 +477,7 @@ public final class GifPagerContentComponent: Component {
|
|||||||
|
|
||||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
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))
|
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 {
|
for (_, itemLayer) in self.visibleItemLayers {
|
||||||
if itemLayer.frame.contains(localPoint) {
|
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.updateVisibleItems(attemptSynchronousLoads: false)
|
||||||
|
|
||||||
self.updateScrollingOffset(transition: .immediate)
|
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>) {
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var validIds = Set<MediaId>()
|
var validIds = Set<ItemKey>()
|
||||||
|
|
||||||
if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) {
|
if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) {
|
||||||
for index in itemRange.lowerBound ..< itemRange.upperBound {
|
for index in itemRange.lowerBound ..< itemRange.upperBound {
|
||||||
let item = component.items[index]
|
var item: Item?
|
||||||
let itemId = item.file.fileId
|
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)
|
validIds.insert(itemId)
|
||||||
|
|
||||||
|
let itemFrame = itemLayout.frame(at: index)
|
||||||
|
|
||||||
|
let itemTransition: Transition = .immediate
|
||||||
|
var updateItemLayerPlaceholder = false
|
||||||
|
|
||||||
let itemLayer: ItemLayer
|
let itemLayer: ItemLayer
|
||||||
if let current = self.visibleItemLayers[itemId] {
|
if let current = self.visibleItemLayers[itemId] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
} else {
|
} else {
|
||||||
|
updateItemLayerPlaceholder = true
|
||||||
|
|
||||||
itemLayer = ItemLayer(
|
itemLayer = ItemLayer(
|
||||||
item: item,
|
item: item,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
groupId: "savedGif",
|
groupId: "savedGif",
|
||||||
attemptSynchronousLoad: attemptSynchronousLoads,
|
attemptSynchronousLoad: attemptSynchronousLoads,
|
||||||
file: item.file,
|
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
|
||||||
placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor
|
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.scrollView.layer.addSublayer(itemLayer)
|
||||||
self.visibleItemLayers[itemId] = 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
|
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 {
|
for (id, itemLayer) in self.visibleItemLayers {
|
||||||
if !validIds.contains(id) {
|
if !validIds.contains(id) {
|
||||||
removedIds.append(id)
|
removedIds.append(id)
|
||||||
itemLayer.removeFromSuperlayer()
|
itemLayer.removeFromSuperlayer()
|
||||||
|
|
||||||
|
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
||||||
|
view.removeFromSuperview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in removedIds {
|
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 {
|
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.component = component
|
||||||
self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme
|
self.theme = keyboardChildEnvironment.theme
|
||||||
|
|
||||||
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
|
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
|
||||||
self.pagerEnvironment = pagerEnvironment
|
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(
|
let itemLayout = ItemLayout(
|
||||||
width: availableSize.width,
|
width: availableSize.width,
|
||||||
containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right),
|
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 {
|
if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets {
|
||||||
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.previousScrollingOffset = self.scrollView.contentOffset.y
|
||||||
self.ignoreScrolling = false
|
self.ignoreScrolling = false
|
||||||
|
|
||||||
|
@ -297,11 +297,11 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
|||||||
|
|
||||||
var targets: [TargetReference] = []
|
var targets: [TargetReference] = []
|
||||||
var slotIndex: Int
|
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.slotIndex = slotIndex
|
||||||
self.preferredBytesPerRow = preferredBytesPerRow
|
self.preferredRowAlignment = preferredRowAlignment
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.stateUpdated = stateUpdated
|
self.stateUpdated = stateUpdated
|
||||||
|
|
||||||
@ -375,10 +375,10 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
|||||||
let readyTextureU = texturePoolHalfPlane.take()
|
let readyTextureU = texturePoolHalfPlane.take()
|
||||||
let readyTextureV = texturePoolHalfPlane.take()
|
let readyTextureV = texturePoolHalfPlane.take()
|
||||||
let readyTextureA = texturePoolFullPlane.take()
|
let readyTextureA = texturePoolFullPlane.take()
|
||||||
let preferredBytesPerRow = self.preferredBytesPerRow
|
let preferredRowAlignment = self.preferredRowAlignment
|
||||||
|
|
||||||
return LoadFrameTask(task: { [weak self] in
|
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 textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane)
|
||||||
let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane)
|
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 texturePoolFullPlane: TextureStoragePool
|
||||||
private let texturePoolHalfPlane: TextureStoragePool
|
private let texturePoolHalfPlane: TextureStoragePool
|
||||||
|
|
||||||
private let preferredBytesPerRow: Int
|
private let preferredRowAlignment: Int
|
||||||
|
|
||||||
private let slotCount: Int
|
private let slotCount: Int
|
||||||
private let slotsX: 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.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.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()
|
super.init()
|
||||||
|
|
||||||
@ -508,7 +508,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
|||||||
for i in 0 ..< self.slotCount {
|
for i in 0 ..< self.slotCount {
|
||||||
if self.slotToItemId[i] == nil {
|
if self.slotToItemId[i] == nil {
|
||||||
self.slotToItemId[i] = itemId
|
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 {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -778,7 +778,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
|||||||
self.isPlaying = isPlaying
|
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)
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16)))
|
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
|
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)
|
completion(false)
|
||||||
|
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
|
@ -6,9 +6,9 @@ import AnimationCache
|
|||||||
import Accelerate
|
import Accelerate
|
||||||
|
|
||||||
public protocol MultiAnimationRenderer: AnyObject {
|
public protocol MultiAnimationRenderer: AnyObject {
|
||||||
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable
|
func add(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 loadFirstFrameSynchronously(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 loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private var nextRenderTargetId: Int64 = 1
|
private var nextRenderTargetId: Int64 = 1
|
||||||
@ -60,6 +60,9 @@ open class MultiAnimationRenderTarget: SimpleLayer {
|
|||||||
|
|
||||||
open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func transitionToContents(_ contents: AnyObject) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class FrameGroup {
|
private final class FrameGroup {
|
||||||
@ -178,8 +181,9 @@ private final class ItemAnimationContext {
|
|||||||
|
|
||||||
func updateAddedTarget(target: MultiAnimationRenderTarget) {
|
func updateAddedTarget(target: MultiAnimationRenderTarget) {
|
||||||
if let currentFrameGroup = self.currentFrameGroup {
|
if let currentFrameGroup = self.currentFrameGroup {
|
||||||
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
if let cgImage = currentFrameGroup.image.cgImage {
|
||||||
target.contents = currentFrameGroup.image.cgImage
|
target.transitionToContents(cgImage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateIsPlaying()
|
self.updateIsPlaying()
|
||||||
@ -239,8 +243,7 @@ private final class ItemAnimationContext {
|
|||||||
strongSelf.currentFrameGroup = currentFrameGroup
|
strongSelf.currentFrameGroup = currentFrameGroup
|
||||||
for target in strongSelf.targets.copyItems() {
|
for target in strongSelf.targets.copyItems() {
|
||||||
if let target = target.value {
|
if let target = target.value {
|
||||||
target.contents = currentFrameGroup.image.cgImage
|
target.transitionToContents(currentFrameGroup.image.cgImage!)
|
||||||
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,7 +403,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||||||
|
|
||||||
public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
|
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 frameSkip: Int
|
||||||
private var displayLink: ConstantDisplayLinkAnimator?
|
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
|
let groupContext: GroupContext
|
||||||
if let current = self.groupContexts[groupId] {
|
if let current = self.groupContext {
|
||||||
groupContext = current
|
groupContext = current
|
||||||
} else {
|
} else {
|
||||||
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
||||||
@ -447,7 +450,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||||||
}
|
}
|
||||||
strongSelf.updateIsPlaying()
|
strongSelf.updateIsPlaying()
|
||||||
})
|
})
|
||||||
self.groupContexts[groupId] = groupContext
|
self.groupContext = groupContext
|
||||||
}
|
}
|
||||||
|
|
||||||
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch)
|
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
|
let groupContext: GroupContext
|
||||||
if let current = self.groupContexts[groupId] {
|
if let current = self.groupContext {
|
||||||
groupContext = current
|
groupContext = current
|
||||||
} else {
|
} else {
|
||||||
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
||||||
@ -468,15 +471,15 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||||||
}
|
}
|
||||||
strongSelf.updateIsPlaying()
|
strongSelf.updateIsPlaying()
|
||||||
})
|
})
|
||||||
self.groupContexts[groupId] = groupContext
|
self.groupContext = groupContext
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size)
|
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
|
let groupContext: GroupContext
|
||||||
if let current = self.groupContexts[groupId] {
|
if let current = self.groupContext {
|
||||||
groupContext = current
|
groupContext = current
|
||||||
} else {
|
} else {
|
||||||
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
||||||
@ -485,7 +488,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||||||
}
|
}
|
||||||
strongSelf.updateIsPlaying()
|
strongSelf.updateIsPlaying()
|
||||||
})
|
})
|
||||||
self.groupContexts[groupId] = groupContext
|
self.groupContext = groupContext
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion)
|
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() {
|
private func updateIsPlaying() {
|
||||||
var isPlaying = false
|
var isPlaying = false
|
||||||
for (_, groupContext) in self.groupContexts {
|
if let groupContext = self.groupContext {
|
||||||
if groupContext.isPlaying {
|
if groupContext.isPlaying {
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,7 +509,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||||||
let secondsPerFrame = Double(self.frameSkip) / 60.0
|
let secondsPerFrame = Double(self.frameSkip) / 60.0
|
||||||
|
|
||||||
var tasks: [LoadFrameGroupTask] = []
|
var tasks: [LoadFrameGroupTask] = []
|
||||||
for (_, groupContext) in self.groupContexts {
|
if let groupContext = self.groupContext {
|
||||||
if groupContext.isPlaying {
|
if groupContext.isPlaying {
|
||||||
tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame))
|
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 TextNodeWithEntities {
|
||||||
public final class Arguments {
|
public final class Arguments {
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
@ -105,6 +117,35 @@ public final class TextNodeWithEntities {
|
|||||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
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)
|
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] {
|
if let current = self.inlineStickerItemLayers[id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
} else {
|
} 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.inlineStickerItemLayers[id] = itemLayer
|
||||||
self.textNode.layer.addSublayer(itemLayer)
|
self.textNode.layer.addSublayer(itemLayer)
|
||||||
|
|
||||||
@ -331,7 +372,7 @@ public class ImmediateTextNodeWithEntities: TextNode {
|
|||||||
if let current = self.inlineStickerItemLayers[id] {
|
if let current = self.inlineStickerItemLayers[id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
} else {
|
} 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.inlineStickerItemLayers[id] = itemLayer
|
||||||
self.layer.addSublayer(itemLayer)
|
self.layer.addSublayer(itemLayer)
|
||||||
|
|
||||||
|
@ -1031,7 +1031,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
var insets: UIEdgeInsets
|
var insets: UIEdgeInsets
|
||||||
var inputPanelBottomInsetTerm: CGFloat = 0.0
|
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: [])
|
insets = layout.insets(options: [])
|
||||||
inputPanelBottomInsetTerm = max(insets.bottom, layout.standardInputHeight)
|
inputPanelBottomInsetTerm = max(insets.bottom, layout.standardInputHeight)
|
||||||
} else {
|
} else {
|
||||||
@ -1191,9 +1196,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
inputNode.hideInputUpdated = { [weak self] transition in
|
inputNode.hideInputUpdated = { [weak self] transition in
|
||||||
self?.updateInputPanelBackgroundExpansion(transition: transition)
|
self?.updateInputPanelBackgroundExpansion(transition: transition)
|
||||||
}
|
}
|
||||||
inputNode.expansionFractionUpdated = { [weak self] transition in
|
|
||||||
self?.updateInputPanelBackgroundExpansion(transition: transition)
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissedInputNode = self.inputNode
|
dismissedInputNode = self.inputNode
|
||||||
if let inputNode = self.inputNode {
|
if let inputNode = self.inputNode {
|
||||||
@ -1236,7 +1238,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
inputNodeHeightAndOverflow = (
|
inputNodeHeightAndOverflow = (
|
||||||
boundedHeight,
|
boundedHeight,
|
||||||
max(0.0, inputHeight - boundedHeight)
|
inputNode.followsDefaultHeight ? max(0.0, inputHeight - boundedHeight) : 0.0
|
||||||
)
|
)
|
||||||
} else if let inputNode = self.inputNode {
|
} else if let inputNode = self.inputNode {
|
||||||
dismissedInputNode = inputNode
|
dismissedInputNode = inputNode
|
||||||
@ -2058,7 +2060,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8)
|
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
|
let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState
|
||||||
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
||||||
@ -2171,18 +2176,22 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated)
|
self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var waitForKeyboardLayout = false
|
||||||
if let textView = self.textInputPanelNode?.textInputNode?.textView {
|
if let textView = self.textInputPanelNode?.textInputNode?.textView {
|
||||||
let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState)
|
let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState)
|
||||||
if textView.inputView !== updatedInputView {
|
if textView.inputView !== updatedInputView {
|
||||||
textView.inputView = updatedInputView
|
textView.inputView = updatedInputView
|
||||||
if textView.isFirstResponder {
|
if textView.isFirstResponder {
|
||||||
|
if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) {
|
||||||
|
waitForKeyboardLayout = true
|
||||||
|
}
|
||||||
textView.reloadInputViews()
|
textView.reloadInputViews()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedInputFocus {
|
if updatedInputFocus {
|
||||||
if !self.ignoreUpdateHeight {
|
if !self.ignoreUpdateHeight && !waitForKeyboardLayout {
|
||||||
self.scheduleLayoutTransitionRequest(layoutTransition)
|
self.scheduleLayoutTransitionRequest(layoutTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,16 +238,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
},
|
},
|
||||||
chatPeerId: chatPeerId
|
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: {
|
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||||
return TempBox.shared.tempFile(fileName: "file").path
|
return TempBox.shared.tempFile(fileName: "file").path
|
||||||
@ -560,22 +550,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
return animatedEmojiStickers
|
return animatedEmojiStickers
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are intentionally not subscribing to the recent gif updates here
|
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||||
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
performItemAction: { [weak controllerInteraction] item, view, rect in
|
||||||
|> map { savedGifs -> GifPagerContentComponent in
|
guard let controllerInteraction = controllerInteraction else {
|
||||||
var items: [GifPagerContentComponent.Item] = []
|
return
|
||||||
for gifItem in savedGifs {
|
}
|
||||||
items.append(GifPagerContentComponent.Item(
|
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
|
||||||
file: gifItem.contents.get(RecentMediaItem.self)!.media
|
},
|
||||||
))
|
openGifContextMenu: { _, _, _, _, _ in
|
||||||
|
},
|
||||||
|
loadMore: { _ in
|
||||||
}
|
}
|
||||||
return GifPagerContentComponent(
|
)
|
||||||
context: context,
|
|
||||||
inputInteraction: gifInputInteraction,
|
// We are going to subscribe to the actual data when the view is loaded
|
||||||
subject: .recent,
|
let gifItems: Signal<GifPagerContentComponent, NoError> = .single(GifPagerContentComponent(
|
||||||
items: items
|
context: context,
|
||||||
)
|
inputInteraction: gifInputInteraction,
|
||||||
}
|
subject: .recent,
|
||||||
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
|
loadMoreToken: nil
|
||||||
|
))
|
||||||
|
|
||||||
return combineLatest(queue: .mainQueue(),
|
return combineLatest(queue: .mainQueue(),
|
||||||
emojiItems,
|
emojiItems,
|
||||||
@ -625,10 +621,195 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
|
|
||||||
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
||||||
didSet {
|
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) {
|
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -639,17 +820,18 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
|
|
||||||
self.entityKeyboardView = ComponentHostView<Empty>()
|
self.entityKeyboardView = ComponentHostView<Empty>()
|
||||||
|
|
||||||
self.gifModeSubject = ValuePromise<GifPagerContentComponent.Subject>(self.gifMode, ignoreRepeated: true)
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.topBackgroundExtension = 41.0
|
||||||
|
self.followsDefaultHeight = true
|
||||||
|
|
||||||
self.view.addSubview(self.entityKeyboardView)
|
self.view.addSubview(self.entityKeyboardView)
|
||||||
|
|
||||||
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
|
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
|
||||||
|
|
||||||
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
updatedInputData,
|
updatedInputData,
|
||||||
self.updatedGifs()
|
self.gifComponent.get()
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in
|
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in
|
||||||
guard let strongSelf = self else {
|
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)
|
self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
||||||
|> map { items -> ChatMediaInputGifPaneTrendingState? in
|
|> map { items -> ChatMediaInputGifPaneTrendingState? in
|
||||||
if let items = items {
|
if let items = items {
|
||||||
@ -693,14 +876,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
self.gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||||
self.inputDataDisposable?.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updatedGifs() -> Signal<GifPagerContentComponent, NoError> {
|
|
||||||
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
|
||||||
performItemAction: { [weak controllerInteraction] item, view, rect in
|
performItemAction: { [weak controllerInteraction] item, view, rect in
|
||||||
guard let controllerInteraction = controllerInteraction else {
|
guard let controllerInteraction = controllerInteraction else {
|
||||||
return
|
return
|
||||||
@ -712,83 +889,26 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
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
|
self.reloadGifContext()
|
||||||
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 {
|
deinit {
|
||||||
for file in trendingGifs.files {
|
self.inputDataDisposable?.dispose()
|
||||||
items.append(GifPagerContentComponent.Item(
|
}
|
||||||
file: file.file.media
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GifPagerContentComponent(
|
private func reloadGifContext() {
|
||||||
context: context,
|
if let gifInputInteraction = self.gifInputInteraction {
|
||||||
inputInteraction: gifInputInteraction,
|
self.gifContext = GifContext(context: self.context, subject: self.gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return .single(self.currentInputData.gifs)
|
|
||||||
|> then(updatedGifs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func markInputCollapsed() {
|
func markInputCollapsed() {
|
||||||
@ -808,7 +928,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
let wasMarkedInputCollapsed = self.isMarkInputCollapsed
|
let wasMarkedInputCollapsed = self.isMarkInputCollapsed
|
||||||
self.isMarkInputCollapsed = false
|
self.isMarkInputCollapsed = false
|
||||||
|
|
||||||
let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight)
|
let expandedHeight = standardInputHeight
|
||||||
|
|
||||||
var hiddenInputHeight: CGFloat = 0.0
|
var hiddenInputHeight: CGFloat = 0.0
|
||||||
if self.hideInput && !self.adjustLayoutForHiddenInput {
|
if self.hideInput && !self.adjustLayoutForHiddenInput {
|
||||||
@ -822,18 +942,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
|
|
||||||
var mappedTransition = Transition(transition)
|
var mappedTransition = Transition(transition)
|
||||||
|
|
||||||
if wasMarkedInputCollapsed {
|
if wasMarkedInputCollapsed || !isExpanded {
|
||||||
mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed())
|
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(
|
let entityKeyboardSize = self.entityKeyboardView.update(
|
||||||
transition: mappedTransition,
|
transition: mappedTransition,
|
||||||
component: AnyComponent(EntityKeyboardComponent(
|
component: AnyComponent(EntityKeyboardComponent(
|
||||||
theme: interfaceState.theme,
|
theme: interfaceState.theme,
|
||||||
bottomInset: bottomInset,
|
bottomInset: bottomInset,
|
||||||
emojiContent: self.currentInputData.emoji,
|
emojiContent: self.currentInputData.emoji,
|
||||||
stickerContent: self.currentInputData.stickers,
|
stickerContent: stickerContent,
|
||||||
gifContent: self.currentInputData.gifs,
|
gifContent: gifContent,
|
||||||
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
|
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
|
||||||
defaultToEmojiTab: self.defaultToEmojiTab,
|
defaultToEmojiTab: self.defaultToEmojiTab,
|
||||||
externalTopPanelContainer: self.externalTopPanelContainerImpl,
|
externalTopPanelContainer: self.externalTopPanelContainerImpl,
|
||||||
@ -868,7 +1007,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.gifModeSubject.set(subject)
|
strongSelf.gifMode = subject
|
||||||
},
|
},
|
||||||
makeSearchContainerNode: { content in
|
makeSearchContainerNode: { content in
|
||||||
let mappedMode: ChatMediaInputSearchMode
|
let mappedMode: ChatMediaInputSearchMode
|
||||||
|
@ -15,15 +15,14 @@ class ChatInputNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var topBackgroundExtension: CGFloat = 41.0
|
var topBackgroundExtension: CGFloat = 0.0
|
||||||
var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
var hideInput: Bool = false
|
var hideInput: Bool = false
|
||||||
var adjustLayoutForHiddenInput: Bool = false
|
var adjustLayoutForHiddenInput: Bool = false
|
||||||
var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
var expansionFraction: CGFloat = 0.0
|
var followsDefaultHeight: Bool = false
|
||||||
var expansionFractionUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
|
||||||
|
|
||||||
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) {
|
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)
|
return (0.0, 0.0)
|
||||||
|
@ -272,15 +272,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
|
|||||||
case .inputButtons:
|
case .inputButtons:
|
||||||
return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||||
case .none, .text:
|
case .none, .text:
|
||||||
if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage {
|
if let _ = chatPresentationInterfaceState.interfaceState.editMessage {
|
||||||
let isTextEmpty = editMessage.inputState.inputText.length == 0
|
accessoryItems.append(.stickers(isEnabled: true, isEmoji: true))
|
||||||
|
|
||||||
let stickersAreEmoji = !isTextEmpty
|
|
||||||
|
|
||||||
var stickersEnabled = true
|
|
||||||
stickersEnabled = true
|
|
||||||
|
|
||||||
accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji))
|
|
||||||
|
|
||||||
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||||
} else {
|
} else {
|
||||||
@ -330,7 +323,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
|
|||||||
accessoryItems.append(.commands)
|
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 {
|
if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id {
|
||||||
accessoryItems.append(.inputButtons)
|
accessoryItems.append(.inputButtons)
|
||||||
|
@ -49,7 +49,7 @@ private final class CachedChatMessageText {
|
|||||||
|
|
||||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
private let textNode: TextNodeWithEntities
|
private let textNode: TextNodeWithEntities
|
||||||
private var spoilerTextNode: TextNode?
|
private var spoilerTextNode: TextNodeWithEntities?
|
||||||
private var dustNode: InvisibleInkDustNode?
|
private var dustNode: InvisibleInkDustNode?
|
||||||
|
|
||||||
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
||||||
@ -67,11 +67,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
switch self.visibility {
|
switch self.visibility {
|
||||||
case .none:
|
case .none:
|
||||||
self.textNode.visibilityRect = nil
|
self.textNode.visibilityRect = nil
|
||||||
|
self.spoilerTextNode?.visibilityRect = nil
|
||||||
case let .visible(_, subRect):
|
case let .visible(_, subRect):
|
||||||
var subRect = subRect
|
var subRect = subRect
|
||||||
subRect.origin.x = 0.0
|
subRect.origin.x = 0.0
|
||||||
subRect.size.width = 10000.0
|
subRect.size.width = 10000.0
|
||||||
self.textNode.visibilityRect = subRect
|
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))) {
|
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 textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||||
let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
|
let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
|
||||||
let statusLayout = self.statusNode.asyncLayout()
|
let statusLayout = self.statusNode.asyncLayout()
|
||||||
|
|
||||||
let currentCachedChatMessageText = self.cachedChatMessageText
|
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 (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 {
|
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 {
|
} else {
|
||||||
spoilerTextLayoutAndApply = nil
|
spoilerTextLayoutAndApply = nil
|
||||||
}
|
}
|
||||||
@ -440,33 +442,33 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
|
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
|
||||||
|
|
||||||
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
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 {
|
if strongSelf.spoilerTextNode == nil {
|
||||||
spoilerTextNode.alpha = 0.0
|
spoilerTextNode.textNode.alpha = 0.0
|
||||||
spoilerTextNode.isUserInteractionEnabled = false
|
spoilerTextNode.textNode.isUserInteractionEnabled = false
|
||||||
spoilerTextNode.contentMode = .topLeft
|
spoilerTextNode.textNode.contentMode = .topLeft
|
||||||
spoilerTextNode.contentsScale = UIScreenScale
|
spoilerTextNode.textNode.contentsScale = UIScreenScale
|
||||||
spoilerTextNode.displaysAsynchronously = false
|
spoilerTextNode.textNode.displaysAsynchronously = false
|
||||||
strongSelf.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
|
strongSelf.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
|
||||||
|
|
||||||
strongSelf.spoilerTextNode = spoilerTextNode
|
strongSelf.spoilerTextNode = spoilerTextNode
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.spoilerTextNode?.frame = textFrame
|
strongSelf.spoilerTextNode?.textNode.frame = textFrame
|
||||||
|
|
||||||
let dustNode: InvisibleInkDustNode
|
let dustNode: InvisibleInkDustNode
|
||||||
if let current = strongSelf.dustNode {
|
if let current = strongSelf.dustNode {
|
||||||
dustNode = current
|
dustNode = current
|
||||||
} else {
|
} else {
|
||||||
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode)
|
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode)
|
||||||
strongSelf.dustNode = dustNode
|
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.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) })
|
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 {
|
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
||||||
strongSelf.spoilerTextNode = nil
|
strongSelf.spoilerTextNode = nil
|
||||||
spoilerTextNode.removeFromSupernode()
|
spoilerTextNode.textNode.removeFromSupernode()
|
||||||
|
|
||||||
if let dustNode = strongSelf.dustNode {
|
if let dustNode = strongSelf.dustNode {
|
||||||
strongSelf.dustNode = nil
|
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 {
|
if let textSelectionNode = strongSelf.textSelectionNode {
|
||||||
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
||||||
textSelectionNode.frame = textFrame
|
textSelectionNode.frame = textFrame
|
||||||
|
@ -56,7 +56,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
private let lineNode: AnimatedNavigationStripeNode
|
private let lineNode: AnimatedNavigationStripeNode
|
||||||
private let titleNode: AnimatedCountLabelNode
|
private let titleNode: AnimatedCountLabelNode
|
||||||
private let textNode: TextNodeWithEntities
|
private let textNode: TextNodeWithEntities
|
||||||
private var spoilerTextNode: TextNode?
|
private var spoilerTextNode: TextNodeWithEntities?
|
||||||
private var dustNode: InvisibleInkDustNode?
|
private var dustNode: InvisibleInkDustNode?
|
||||||
private let actionButton: HighlightableButtonNode
|
private let actionButton: HighlightableButtonNode
|
||||||
private let actionButtonTitleNode: ImmediateTextNode
|
private let actionButtonTitleNode: ImmediateTextNode
|
||||||
@ -525,7 +525,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
|
|
||||||
let makeTitleLayout = self.titleNode.asyncLayout()
|
let makeTitleLayout = self.titleNode.asyncLayout()
|
||||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||||
let makeSpoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
|
let makeSpoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
|
||||||
let imageNodeLayout = self.imageNode.asyncLayout()
|
let imageNodeLayout = self.imageNode.asyncLayout()
|
||||||
|
|
||||||
let previousMediaReference = self.previousMediaReference
|
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 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 (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 {
|
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 {
|
} else {
|
||||||
spoilerTextLayoutAndApply = nil
|
spoilerTextLayoutAndApply = nil
|
||||||
}
|
}
|
||||||
@ -701,33 +701,33 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
strongSelf.textNode.textNode.frame = textFrame
|
strongSelf.textNode.textNode.frame = textFrame
|
||||||
|
|
||||||
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
||||||
let spoilerTextNode = spoilerTextApply()
|
let spoilerTextNode = spoilerTextApply(textArguments)
|
||||||
if strongSelf.spoilerTextNode == nil {
|
if strongSelf.spoilerTextNode == nil {
|
||||||
spoilerTextNode.alpha = 0.0
|
spoilerTextNode.textNode.alpha = 0.0
|
||||||
spoilerTextNode.isUserInteractionEnabled = false
|
spoilerTextNode.textNode.isUserInteractionEnabled = false
|
||||||
spoilerTextNode.contentMode = .topLeft
|
spoilerTextNode.textNode.contentMode = .topLeft
|
||||||
spoilerTextNode.contentsScale = UIScreenScale
|
spoilerTextNode.textNode.contentsScale = UIScreenScale
|
||||||
spoilerTextNode.displaysAsynchronously = false
|
spoilerTextNode.textNode.displaysAsynchronously = false
|
||||||
strongSelf.contentTextContainer.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textNode.textNode)
|
strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode)
|
||||||
|
|
||||||
strongSelf.spoilerTextNode = spoilerTextNode
|
strongSelf.spoilerTextNode = spoilerTextNode
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.spoilerTextNode?.frame = textFrame
|
strongSelf.spoilerTextNode?.textNode.frame = textFrame
|
||||||
|
|
||||||
let dustNode: InvisibleInkDustNode
|
let dustNode: InvisibleInkDustNode
|
||||||
if let current = strongSelf.dustNode {
|
if let current = strongSelf.dustNode {
|
||||||
dustNode = current
|
dustNode = current
|
||||||
} else {
|
} else {
|
||||||
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode)
|
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode)
|
||||||
strongSelf.dustNode = dustNode
|
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.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) })
|
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 {
|
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
||||||
strongSelf.spoilerTextNode = nil
|
strongSelf.spoilerTextNode = nil
|
||||||
spoilerTextNode.removeFromSupernode()
|
spoilerTextNode.textNode.removeFromSupernode()
|
||||||
|
|
||||||
if let dustNode = strongSelf.dustNode {
|
if let dustNode = strongSelf.dustNode {
|
||||||
strongSelf.dustNode = nil
|
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))
|
let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight))
|
||||||
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
|
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
|
||||||
strongSelf.lineNode.update(
|
strongSelf.lineNode.update(
|
||||||
|
@ -619,10 +619,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self.menuButton.cornerRadius = 16.0
|
self.menuButton.cornerRadius = 16.0
|
||||||
self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu
|
self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu
|
||||||
self.menuButtonBackgroundNode = ASDisplayNode()
|
self.menuButtonBackgroundNode = ASDisplayNode()
|
||||||
|
self.menuButtonBackgroundNode.isUserInteractionEnabled = false
|
||||||
self.menuButtonClippingNode = ASDisplayNode()
|
self.menuButtonClippingNode = ASDisplayNode()
|
||||||
self.menuButtonClippingNode.clipsToBounds = true
|
self.menuButtonClippingNode.clipsToBounds = true
|
||||||
|
self.menuButtonClippingNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.menuButtonIconNode = MenuIconNode()
|
self.menuButtonIconNode = MenuIconNode()
|
||||||
|
self.menuButtonIconNode.isUserInteractionEnabled = false
|
||||||
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
|
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
|
||||||
self.menuButtonTextNode = ImmediateTextNode()
|
self.menuButtonTextNode = ImmediateTextNode()
|
||||||
|
|
||||||
@ -1782,6 +1785,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
|
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
|
||||||
|
|
||||||
if let textInputNode = self.textInputNode {
|
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 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
|
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
|
||||||
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
||||||
@ -2069,7 +2073,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
let endIndex = currentIndex
|
let endIndex = currentIndex
|
||||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
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) {
|
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)
|
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
||||||
for textRect in textRects {
|
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)
|
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
|
accountSettingsController.tabBarItemDebugTapAction = { [weak self] in
|
||||||
guard let strongSelf = self, let accountSettingsController = accountSettingsController else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accountSettingsController.push(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
|
strongSelf.pushViewController(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
|
||||||
}
|
}
|
||||||
controllers.append(accountSettingsController)
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,15 +533,6 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
|||||||
}
|
}
|
||||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
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))
|
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) {
|
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