diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 55d5d295a4..d747d0be7d 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -108,6 +108,33 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground } } +private final class EntityInputView: UIInputView, UIInputViewAudioFeedback { + override var inputViewStyle: UIInputView.Style { + get { + return .default + } + } + override var allowsSelfSizing: Bool { + get { + return true + } + set { + } + } + + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: 300) + } + + override func sizeToFit() { + print("sizeToFit") + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + return CGSize(width: size.width, height: 100.0) + } +} + private class CaptionEditableTextNode: EditableTextNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let previousAlpha = self.alpha @@ -437,6 +464,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.view.addGestureRecognizer(recognizer) textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + + /*let entityInputView = EntityInputView() + entityInputView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)) + entityInputView.backgroundColor = .blue + textInputNode.textView.inputView = entityInputView*/ } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index e76b1d04a2..564db9cc8b 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -656,6 +656,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, var textFrame = self.textFieldFrame textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) textFrame.size.height = self.textInputNode.textView.contentSize.height + textFrame.size.width -= self.textInputNode.textContainerInset.right if self.textInputNode.isRTL { textFrame.origin.x -= messageOriginDelta diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index aa06aec2fa..b14ec8d808 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -381,9 +381,10 @@ public final class PagerComponent TextNodeLayoutArguments { @@ -198,7 +201,8 @@ public final class TextNodeLayoutArguments { lineColor: self.lineColor, textShadowColor: self.textShadowColor, textStroke: self.textStroke, - displaySpoilers: self.displaySpoilers + displaySpoilers: self.displaySpoilers, + displayEmbeddedItemsUnderSpoilers: self.displayEmbeddedItemsUnderSpoilers ) } } @@ -972,7 +976,7 @@ open class TextNode: ASDisplayNode { } } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout { + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool) -> TextNodeLayout { if let attributedString = attributedString { let stringLength = attributedString.length @@ -1126,10 +1130,6 @@ open class TextNode: ASDisplayNode { rightOffset = ceil(secondaryRightOffset) } - if embeddedItems.count > 25 { - assert(true) - } - embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) } @@ -1140,9 +1140,6 @@ open class TextNode: ASDisplayNode { isLastLine = true } if isLastLine { - if attributedString.string.hasPrefix("😀") { - assert(true) - } if first { first = false } else { @@ -1224,12 +1221,6 @@ open class TextNode: ASDisplayNode { } addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1238,6 +1229,16 @@ open class TextNode: ASDisplayNode { } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { headIndent = paragraphStyle.headIndent } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } + } } } @@ -1311,12 +1312,6 @@ open class TextNode: ASDisplayNode { } addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1325,6 +1320,16 @@ open class TextNode: ASDisplayNode { } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { headIndent = paragraphStyle.headIndent } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } + } } let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) @@ -1586,11 +1591,11 @@ open class TextNode: ASDisplayNode { if stringMatch { layout = existingLayout } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) updated = true } } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) updated = true } @@ -2231,11 +2236,11 @@ open class TextView: UIView { if stringMatch { layout = existingLayout } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) updated = true } } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) updated = true } diff --git a/submodules/MtProtoKit/Sources/MTDatacenterAuthAction.m b/submodules/MtProtoKit/Sources/MTDatacenterAuthAction.m index fca53580cf..acf6af66c9 100644 --- a/submodules/MtProtoKit/Sources/MTDatacenterAuthAction.m +++ b/submodules/MtProtoKit/Sources/MTDatacenterAuthAction.m @@ -87,6 +87,8 @@ MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth]; authService.delegate = self; [_authMtProto addMessageService:authService]; + + [_authMtProto resume]; } } else diff --git a/submodules/MtProtoKit/Sources/MTDatacenterTransferAuthAction.m b/submodules/MtProtoKit/Sources/MTDatacenterTransferAuthAction.m index cf1740f7af..54e2de78ad 100644 --- a/submodules/MtProtoKit/Sources/MTDatacenterTransferAuthAction.m +++ b/submodules/MtProtoKit/Sources/MTDatacenterTransferAuthAction.m @@ -91,6 +91,8 @@ requestService.forceBackgroundRequests = true; [_sourceDatacenterMtProto addMessageService:requestService]; + [_sourceDatacenterMtProto resume]; + MTRequest *request = [[MTRequest alloc] init]; NSData *exportAuthRequestData = nil; @@ -130,6 +132,8 @@ requestService.forceBackgroundRequests = true; [_destinationDatacenterMtProto addMessageService:requestService]; + [_destinationDatacenterMtProto resume]; + MTRequest *request = [[MTRequest alloc] init]; NSData *importAuthRequestData = [_context.serialization importAuthorization:dataId bytes:authData]; diff --git a/submodules/MtProtoKit/Sources/MTDiscoverDatacenterAddressAction.m b/submodules/MtProtoKit/Sources/MTDiscoverDatacenterAddressAction.m index 45b10c9bed..8cd1ed0a6f 100644 --- a/submodules/MtProtoKit/Sources/MTDiscoverDatacenterAddressAction.m +++ b/submodules/MtProtoKit/Sources/MTDiscoverDatacenterAddressAction.m @@ -96,6 +96,8 @@ _requestService.forceBackgroundRequests = true; [_mtProto addMessageService:_requestService]; + [_mtProto resume]; + MTRequest *request = [[MTRequest alloc] init]; NSData *getConfigData = nil; diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index dc89d6b73c..5d892618ae 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -171,9 +171,11 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; _sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context]; - - _shouldStayConnected = true; + + _mtState |= MTProtoStatePaused; + + [self setMtState:_mtState | MTProtoStatePaused]; } return self; } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 9335c9ee04..78ea869c99 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -532,12 +532,25 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef return thumbnail |> mapToSignal { thumbnailData in - return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath) - |> map { fullSize, reducedSize in - if !fullSize._1 && reducedSize._1 { - return Tuple(thumbnailData, reducedSize._0, false) + if synchronousLoad, let thumbnailData = thumbnailData { + return .single(Tuple(thumbnailData, nil, false)) + |> then( + combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath) + |> map { fullSize, reducedSize in + if !fullSize._1 && reducedSize._1 { + return Tuple(thumbnailData, reducedSize._0, false) + } + return Tuple(thumbnailData, fullSize._0, fullSize._1) + } + ) + } else { + return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath) + |> map { fullSize, reducedSize in + if !fullSize._1 && reducedSize._1 { + return Tuple(thumbnailData, reducedSize._0, false) + } + return Tuple(thumbnailData, fullSize._0, fullSize._1) } - return Tuple(thumbnailData, fullSize._0, fullSize._1) } } } diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index 2510fad261..685dbb6e09 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -13,7 +13,7 @@ private struct ScanFilesResult { var totalSize: UInt64 = 0 } -private func printOpenFiles() { +public func printOpenFiles() { var flags: Int32 = 0 var fd: Int32 = 0 var buf = Data(count: Int(MAXPATHLEN) + 1) diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index c91d0e70b2..af41c83362 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -197,24 +197,23 @@ open class TabBarControllerImpl: ViewController, TabBarController { return } - if strongSelf.selectedIndex == index { - let timestamp = CACurrentMediaTime() - if strongSelf.debugTapCounter.0 < timestamp - 0.4 { - strongSelf.debugTapCounter.0 = timestamp - strongSelf.debugTapCounter.1 = 0 - } - - if strongSelf.debugTapCounter.0 >= timestamp - 0.4 { - strongSelf.debugTapCounter.0 = timestamp - strongSelf.debugTapCounter.1 += 1 - } - - if strongSelf.debugTapCounter.1 >= 10 { - strongSelf.debugTapCounter.1 = 0 - - strongSelf.controllers[index].tabBarItemDebugTapAction?() - } + let timestamp = CACurrentMediaTime() + if strongSelf.debugTapCounter.0 < timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 = 0 } + + if strongSelf.debugTapCounter.0 >= timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 += 1 + } + + if strongSelf.debugTapCounter.1 >= 10 { + strongSelf.debugTapCounter.1 = 0 + + strongSelf.controllers[index].tabBarItemDebugTapAction?() + } + if let validLayout = strongSelf.validLayout { var updatedLayout = validLayout diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index ebe9762f3f..52387c90db 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -591,6 +591,10 @@ private final class MultipartFetchManager { if totalTime > 0.0 { let speed = Double(totalByteCount) / totalTime Logger.shared.log("MultipartFetch", "\(self.resource.id.stringRepresentation) \(speed) bytes/s") + + #if DEBUG + self.checkState() + #endif } } } diff --git a/submodules/TelegramUI/Components/AnimationCache/ImageDCT/PublicHeaders/ImageDCT/YuvConversion.h b/submodules/TelegramUI/Components/AnimationCache/ImageDCT/PublicHeaders/ImageDCT/YuvConversion.h index a7ceddecb9..81a3ee6569 100644 --- a/submodules/TelegramUI/Components/AnimationCache/ImageDCT/PublicHeaders/ImageDCT/YuvConversion.h +++ b/submodules/TelegramUI/Components/AnimationCache/ImageDCT/PublicHeaders/ImageDCT/YuvConversion.h @@ -5,5 +5,6 @@ void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow); void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow); +void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow); #endif /* YuvConversion_h */ diff --git a/submodules/TelegramUI/Components/AnimationCache/ImageDCT/Sources/YuvConversion.m b/submodules/TelegramUI/Components/AnimationCache/ImageDCT/Sources/YuvConversion.m index 41bd589754..0851a92c91 100644 --- a/submodules/TelegramUI/Components/AnimationCache/ImageDCT/Sources/YuvConversion.m +++ b/submodules/TelegramUI/Components/AnimationCache/ImageDCT/Sources/YuvConversion.m @@ -97,3 +97,19 @@ void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const error = vImageOverwriteChannels_ARGB8888(&srcA, &destArgb, &destArgb, 1 << 0, kvImageDoNotTile); } + +void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow) { + vImage_Buffer src; + src.data = (void *)inPlane; + src.width = inWidth; + src.height = inHeight; + src.rowBytes = inBytesPerRow; + + vImage_Buffer dst; + dst.data = (void *)outPlane; + dst.width = outWidth; + dst.height = outHeight; + dst.rowBytes = outBytesPerRow; + + vImageScale_Planar8(&src, &dst, nil, kvImageDoNotTile); +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index f5ca8fa4b2..09c9af7a1c 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -15,7 +15,7 @@ private func alignUp(size: Int, align: Int) -> Int { public final class AnimationCacheItemFrame { public enum RequestedFormat { case rgba - case yuva(bytesPerRow: Int) + case yuva(rowAlignment: Int) } public final class Plane { @@ -50,11 +50,17 @@ public final class AnimationCacheItem { public let numFrames: Int private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? private let getFrameIndexImpl: (Double) -> Int + private let getFrameDurationImpl: (Int) -> Double? - public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int) { + public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int, getFrameDurationImpl: @escaping (Int) -> Double?) { self.numFrames = numFrames self.getFrameImpl = getFrame self.getFrameIndexImpl = getFrameIndexImpl + self.getFrameDurationImpl = getFrameDurationImpl + } + + public func getFrameDuration(index: Int) -> Double? { + return self.getFrameDurationImpl(index) } public func getFrame(index: Int, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { @@ -123,7 +129,7 @@ private func md5Hash(_ string: String) -> String { } } -private func itemSubpath(hashString: String) -> (directory: String, fileName: String) { +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { assert(hashString.count == 32) var directory = "" @@ -134,7 +140,7 @@ private func itemSubpath(hashString: String) -> (directory: String, fileName: St directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) } - return (directory, hashString) + return (directory, "\(hashString)_\(width)x\(height)") } private func roundUp(_ numToRound: Int, multiple: Int) -> Int { @@ -209,6 +215,213 @@ private func decompressData(data: Data, range: Range, decompressedSize: Int return decompressedFrameData } +private final class AnimationCacheItemWriterInternal { + struct CompressedResult { + var path: String + } + + private struct FrameMetadata { + var offset: Int + var length: Int + var duration: Double + } + + var isCancelled: Bool = false + + private let decompressedPath: String + private let compressedPath: String + private var file: ManagedFile? + + private var currentYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var currentDctCoefficients: DctCoefficientsYUVA420? + private var contentLengthOffset: Int? + private var isFailed: Bool = false + private var isFinished: Bool = false + + private var frames: [FrameMetadata] = [] + private var contentLength: Int = 0 + + private let dctQuality: Int + + init?(allocateTempFile: @escaping () -> String) { + self.dctQuality = 70 + + self.decompressedPath = allocateTempFile() + self.compressedPath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else { + return nil + } + self.file = file + } + + func add(with drawingBlock: (ImageYUVA420) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) { + if self.isFailed || self.isFinished { + return + } + + guard !self.isFailed, !self.isFinished, let file = self.file else { + return + } + + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedWidth, multiple: 16) + + var isFirstFrame = false + + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + return + } + } else { + isFirstFrame = true + yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) + self.currentYUVASurface = yuvaSurface + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + return + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let dctData: DctData + if let current = self.currentDctData, current.quality == self.dctQuality { + dctData = current + } else { + dctData = DctData(quality: self.dctQuality) + self.currentDctData = dctData + } + + drawingBlock(yuvaSurface) + + yuvaSurface.dct(dctData: dctData, target: dctCoefficients) + + if isFirstFrame { + file.write(2 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + file.write(UInt32(dctData.quality)) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + let framePosition = Int(file.position()) + assert(framePosition >= 0) + var frameLength = 0 + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + dctPlane.data.withUnsafeBytes { bytes in + let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameLength += dctPlane.data.count + } + + self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration)) + + self.contentLength += frameLength + } + + func finish() -> CompressedResult? { + var shouldComplete = false + + outer: for _ in 0 ..< 1 { + if !self.isFinished { + self.isFinished = true + shouldComplete = true + + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else { + self.isFailed = true + break outer + } + assert(contentLengthOffset >= 0) + + let metadataPosition = file.position() + file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(self.contentLength)) + + file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(UInt32(frame.offset)) + file.write(UInt32(frame.length)) + file.write(Float32(frame.duration)) + } + + if !self.frames.isEmpty { + } else { + self.isFailed = true + break outer + } + + if !self.isFailed { + self.file = nil + + file._unsafeClose() + + guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else { + self.isFailed = true + break outer + } + guard let compressedData = compressData(data: uncompressedData) else { + self.isFailed = true + break outer + } + guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + self.isFailed = true + break outer + } + compressedFile.write(Int32(uncompressedData.count)) + let _ = compressedFile.write(compressedData) + compressedFile._unsafeClose() + } + } + } + + if shouldComplete { + let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath) + + if !self.isFailed { + return CompressedResult(path: self.compressedPath) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + + return nil + } + } else { + return nil + } + } +} + private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { struct CompressedResult { var animationPath: String @@ -246,7 +459,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { private let lock = Lock() init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { - self.dctQuality = 67 + self.dctQuality = 70 self.queue = queue self.decompressedPath = allocateTempFile() @@ -286,7 +499,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { } else { isFirstFrame = true - surface = ImageARGB(width: width, height: height, bytesPerRow: alignUp(size: width * 4, align: 32)) + surface = ImageARGB(width: width, height: height, rowAlignment: 32) self.currentSurface = surface } @@ -299,7 +512,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { return } } else { - yuvaSurface = ImageYUVA420(width: width, height: height, bytesPerRow: nil) + yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) self.currentYUVASurface = yuvaSurface } @@ -556,17 +769,17 @@ private final class AnimationCacheItemAccessor { if let currentYUVASurface = self.currentYUVASurface { yuvaSurface = currentYUVASurface } else { - yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: nil) + yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: nil) } - case let .yuva(preferredBytesPerRow): - yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: preferredBytesPerRow) + case let .yuva(preferredRowAlignment): + yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment) } self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface) switch requestedFormat { case .rgba: - let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, bytesPerRow: alignUp(size: yuvaSurface.yPlane.width * 4, align: 32)) + let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, rowAlignment: 32) yuvaSurface.toARGB(target: currentSurface) self.currentYUVASurface = yuvaSurface @@ -619,6 +832,14 @@ private final class AnimationCacheItemAccessor { } return self.durationMapping.count - 1 } + + func getFrameDuration(index: Int) -> Double? { + if index < self.durationMapping.count { + return self.durationMapping[index] + } else { + return nil + } + } } private func readUInt32(data: Data, offset: Int) -> UInt32 { @@ -747,9 +968,100 @@ private func loadItem(path: String) -> AnimationCacheItem? { return itemAccessor.getFrame(index: index, requestedFormat: requestedFormat) }, getFrameIndexImpl: { duration in return itemAccessor.getFrameIndex(duration: duration) + }, getFrameDurationImpl: { index in + return itemAccessor.getFrameDuration(index: index) }) } +private func adaptItemFromHigherResolution(itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? { + guard let higherResolutionItem = loadItem(path: higherResolutionPath) else { + return nil + } + guard let writer = AnimationCacheItemWriterInternal(allocateTempFile: allocateTempFile) else { + return nil + } + + for i in 0 ..< higherResolutionItem.numFrames { + guard let duration = higherResolutionItem.getFrameDuration(index: i) else { + break + } + writer.add(with: { yuva in + guard let frame = higherResolutionItem.getFrame(index: i, requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { + return + } + switch frame.format { + case .rgba: + return + case let .yuva(y, u, v, a): + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + } + }, proposedWidth: width, proposedHeight: height, duration: duration) + } + + guard let result = writer.finish() else { + return nil + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return nil + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else { + return nil + } + guard let item = loadItem(path: itemPath) else { + return nil + } + return item +} + +private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { + var candidates: [(path: String, width: Int, height: Int)] = [] + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + let fileName = url.lastPathComponent + if fileName.hasPrefix(baseName) { + let scanner = Scanner(string: fileName) + guard scanner.scanString(baseName, into: nil) else { + continue + } + var itemWidth: Int = 0 + guard scanner.scanInt(&itemWidth) else { + continue + } + guard scanner.scanString("x", into: nil) else { + continue + } + var itemHeight: Int = 0 + guard scanner.scanInt(&itemHeight) else { + continue + } + if !baseSuffix.isEmpty { + guard scanner.scanString(baseSuffix, into: nil) else { + continue + } + } + guard scanner.isAtEnd else { + continue + } + if itemWidth > width && itemHeight > height { + candidates.append((url.path, itemWidth, itemHeight)) + } + } + } + } + if !candidates.isEmpty { + candidates.sort(by: { $0.width < $1.width }) + return candidates[0].path + } + return nil +} + public final class AnimationCacheImpl: AnimationCache { private final class Impl { private final class ItemContext { @@ -789,7 +1101,7 @@ public final class AnimationCacheImpl: AnimationCache { } func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" @@ -878,43 +1190,58 @@ public final class AnimationCacheImpl: AnimationCache { } } - static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" if FileManager.default.fileExists(atPath: itemFirstFramePath) { return loadItem(path: itemFirstFramePath) - } else { - return nil } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) { + return adaptedItem + } + } + + return nil } - static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) { completion(item) - - return EmptyDisposable - } else { - completion(nil) - return EmptyDisposable } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) { + completion(adaptedItem) + return EmptyDisposable + } + } + + completion(nil) + return EmptyDisposable } } private let queue: Queue private let basePath: String private let impl: QueueLocalObject + private let allocateTempFile: () -> String public init(basePath: String, allocateTempFile: @escaping () -> String) { let queue = Queue() self.queue = queue self.basePath = basePath + self.allocateTempFile = allocateTempFile self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile) }) @@ -939,15 +1266,16 @@ public final class AnimationCacheImpl: AnimationCache { } public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { - return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size) + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile) } public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { let disposable = MetaDisposable() let basePath = self.basePath + let allocateTempFile = self.allocateTempFile queue.async { - disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, completion: completion)) + disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, completion: completion)) } return disposable diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift index add500560c..6a31392a13 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift @@ -2,27 +2,46 @@ import Foundation import UIKit import ImageDCT +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + final class ImagePlane { let width: Int let height: Int let bytesPerRow: Int + let rowAlignment: Int let components: Int var data: Data - init(width: Int, height: Int, components: Int, bytesPerRow: Int?) { + init(width: Int, height: Int, components: Int, rowAlignment: Int?) { self.width = width self.height = height - self.bytesPerRow = bytesPerRow ?? (width * components) + self.rowAlignment = rowAlignment ?? 1 + self.bytesPerRow = alignUp(size: width * components, align: self.rowAlignment) self.components = components self.data = Data(count: self.bytesPerRow * height) } } +extension ImagePlane { + func copyScaled(fromPlane plane: AnimationCacheItemFrame.Plane) { + self.data.withUnsafeMutableBytes { destBytes in + plane.data.withUnsafeBytes { srcBytes in + scaleImagePlane(destBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(self.width), Int32(self.height), Int32(self.bytesPerRow), srcBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(plane.width), Int32(plane.height), Int32(plane.bytesPerRow)) + } + } + } +} + final class ImageARGB { let argbPlane: ImagePlane - init(width: Int, height: Int, bytesPerRow: Int?) { - self.argbPlane = ImagePlane(width: width, height: height, components: 4, bytesPerRow: bytesPerRow) + init(width: Int, height: Int, rowAlignment: Int?) { + self.argbPlane = ImagePlane(width: width, height: height, components: 4, rowAlignment: rowAlignment) } } @@ -32,11 +51,11 @@ final class ImageYUVA420 { let vPlane: ImagePlane let aPlane: ImagePlane - init(width: Int, height: Int, bytesPerRow: Int?) { - self.yPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow) - self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow) - self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow) - self.aPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow) + init(width: Int, height: Int, rowAlignment: Int?) { + self.yPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment) + self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment) + self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment) + self.aPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment) } } @@ -92,8 +111,8 @@ extension ImageARGB { } } - func toYUVA420(bytesPerRow: Int?) -> ImageYUVA420 { - let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, bytesPerRow: bytesPerRow) + func toYUVA420(rowAlignment: Int?) -> ImageYUVA420 { + let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, rowAlignment: rowAlignment) self.toYUVA420(target: resultImage) return resultImage } @@ -125,8 +144,8 @@ extension ImageYUVA420 { } } - func toARGB(bytesPerRow: Int?) -> ImageARGB { - let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow) + func toARGB(rowAlignment: Int?) -> ImageARGB { + let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment) self.toARGB(target: resultImage) return resultImage } @@ -221,8 +240,8 @@ extension DctCoefficientsYUVA420 { } } - func idct(dctData: DctData, bytesPerRow: Int?) -> ImageYUVA420 { - let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow) + func idct(dctData: DctData, rowAlignment: Int?) -> ImageYUVA420 { + let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment) self.idct(dctData: dctData, target: resultImage) return resultImage } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 90f7314679..2e0f126b61 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -30,7 +30,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } private let context: AccountContext - private let groupId: String private let emoji: ChatTextInputTextCustomEmojiAttribute private let cache: AnimationCache private let renderer: MultiAnimationRenderer @@ -39,6 +38,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private let pointSize: CGSize private let pixelSize: CGSize + private var isDisplayingPlaceholder: Bool = false + private var file: TelegramMediaFile? private var infoDisposable: Disposable? private var disposable: Disposable? @@ -54,9 +55,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } - public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { + public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { self.context = context - self.groupId = groupId self.emoji = emoji self.cache = cache self.renderer = renderer @@ -123,9 +123,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.file = file if attemptSynchronousLoad { - if !self.renderer.loadFirstFrameSynchronously(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) { + if !self.renderer.loadFirstFrameSynchronously(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) { if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) { self.contents = image.cgImage + self.isDisplayingPlaceholder = true } } @@ -133,10 +134,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } else { let pointSize = self.pointSize let placeholderColor = self.placeholderColor - self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in + self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in if !result { MultiAnimationRendererImpl.firstFrameQueue.async { - let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) + let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { guard let strongSelf = self else { @@ -144,6 +145,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } if let image = image { strongSelf.contents = image.cgImage + strongSelf.isDisplayingPlaceholder = true } strongSelf.loadAnimation() } @@ -165,7 +167,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let context = self.context if file.isAnimatedSticker || file.isVideoEmoji { - self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in + self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in @@ -192,7 +194,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } }) } else { - self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in + self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in guard result.complete else { return @@ -210,13 +212,45 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { }) } } + + override public func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.isDisplayingPlaceholder == displayPlaceholder { + return + } + self.isDisplayingPlaceholder = displayPlaceholder + } + + override public func transitionToContents(_ contents: AnyObject) { + if self.isDisplayingPlaceholder { + self.isDisplayingPlaceholder = false + + if let current = self.contents { + let previousLayer = SimpleLayer() + previousLayer.contents = current + previousLayer.frame = self.frame + self.superlayer?.insertSublayer(previousLayer, below: self) + previousLayer.opacity = 0.0 + previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in + previousLayer?.removeFromSuperlayer() + }) + + self.contents = contents + self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else { + self.contents = contents + self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + self.contents = contents + } + } } public final class EmojiTextAttachmentView: UIView { private let contentLayer: InlineStickerItemLayer public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { - self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0)) + self.contentLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0)) super.init(frame: CGRect()) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 09fd0b9db5..903aa9d4d6 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -537,12 +537,11 @@ public final class EmojiPagerContentComponent: Component { } } private(set) var displayPlaceholder: Bool = false - let onUpdateDisplayPlaceholder: (Bool) -> Void + let onUpdateDisplayPlaceholder: (Bool, Double) -> Void init( item: Item, context: AccountContext, - groupId: String, attemptSynchronousLoad: Bool, file: TelegramMediaFile?, staticEmoji: String?, @@ -552,7 +551,7 @@ public final class EmojiPagerContentComponent: Component { blurredBadgeColor: UIColor, displayPremiumBadgeIfAvailable: Bool, pointSize: CGSize, - onUpdateDisplayPlaceholder: @escaping (Bool) -> Void + onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void ) { self.item = item self.file = file @@ -573,7 +572,7 @@ public final class EmojiPagerContentComponent: Component { return } - strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in @@ -604,13 +603,13 @@ public final class EmojiPagerContentComponent: Component { } if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { self.updateDisplayPlaceholder(displayPlaceholder: true) } loadAnimation() } else { - let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in + let _ = renderer.loadFirstFrame(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in loadAnimation() if !success { @@ -682,7 +681,7 @@ public final class EmojiPagerContentComponent: Component { self.placeholderColor = layer.placeholderColor self.size = layer.size - self.onUpdateDisplayPlaceholder = { _ in } + self.onUpdateDisplayPlaceholder = { _, _ in } super.init(layer: layer) } @@ -718,47 +717,17 @@ public final class EmojiPagerContentComponent: Component { } self.displayPlaceholder = displayPlaceholder - self.onUpdateDisplayPlaceholder(displayPlaceholder) + self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0) + } + + override func transitionToContents(_ contents: AnyObject) { + self.contents = contents - /*if displayPlaceholder { - if self.placeholderView == nil { - self.placeholderView = PortalView() - if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView { - self.addSublayer(placeholderView.view.layer) - placeholderView.view.frame = self.bounds - shimmerView.addPortal(view: placeholderView) - } - } - if self.placeholderMaskLayer == nil { - self.placeholderMaskLayer = SimpleLayer() - self.placeholderView?.view.layer.mask = self.placeholderMaskLayer - } - let file = self.file - let size = self.size - //let placeholderColor = self.placeholderColor - - Queue.concurrentDefaultQueue().async { [weak self] in - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: .black) { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - - if strongSelf.displayPlaceholder { - strongSelf.placeholderMaskLayer?.contents = image.cgImage - } - } - } - } - } else { - if let placeholderView = self.placeholderView { - self.placeholderView = nil - placeholderView.view.layer.removeFromSuperlayer() - } - if let _ = self.placeholderMaskLayer { - self.placeholderMaskLayer = nil - } - }*/ + if self.displayPlaceholder { + self.displayPlaceholder = false + self.onUpdateDisplayPlaceholder(false, 0.2) + self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } } } @@ -790,6 +759,7 @@ public final class EmojiPagerContentComponent: Component { private let boundsChangeTrackerLayer = SimpleLayer() private var effectiveVisibleSize: CGSize = CGSize() + private let placeholdersContainerView: UIView private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:] @@ -812,12 +782,13 @@ public final class EmojiPagerContentComponent: Component { override init(frame: CGRect) { self.shimmerHostView = PortalSourceView() - self.standaloneShimmerEffect = StandaloneShimmerEffect() self.scrollView = ContentScrollView() self.scrollView.layer.anchorPoint = CGPoint() + self.placeholdersContainerView = UIView() + super.init(frame: frame) self.shimmerHostView.alpha = 0.0 @@ -842,6 +813,8 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.clipsToBounds = false self.addSubview(self.scrollView) + self.scrollView.addSubview(self.placeholdersContainerView) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in @@ -1404,7 +1377,6 @@ public final class EmojiPagerContentComponent: Component { itemLayer = ItemLayer( item: item, context: component.context, - groupId: "keyboard-\(Int(itemLayout.nativeItemSize))", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, staticEmoji: item.staticEmoji, @@ -1414,7 +1386,7 @@ public final class EmojiPagerContentComponent: Component { blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges, pointSize: itemNativeFitSize, - onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in guard let strongSelf = self else { return } @@ -1432,7 +1404,7 @@ public final class EmojiPagerContentComponent: Component { size: itemNativeFitSize ) strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView - strongSelf.scrollView.insertSubview(placeholderView, at: 0) + strongSelf.placeholdersContainerView.addSubview(placeholderView) } placeholderView.frame = itemLayer.frame placeholderView.update(size: placeholderView.bounds.size) @@ -1442,9 +1414,20 @@ public final class EmojiPagerContentComponent: Component { } else { if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) - placeholderView.removeFromSuperview() - strongSelf.updateShimmerIfNeeded() + if duration > 0.0 { + placeholderView.layer.opacity = 0.0 + placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in + guard let strongSelf = self else { + return + } + placeholderView?.removeFromSuperview() + strongSelf.updateShimmerIfNeeded() + }) + } else { + placeholderView.removeFromSuperview() + strongSelf.updateShimmerIfNeeded() + } } } } @@ -1471,7 +1454,7 @@ public final class EmojiPagerContentComponent: Component { } } else if updateItemLayerPlaceholder { if itemLayer.displayPlaceholder { - itemLayer.onUpdateDisplayPlaceholder(true) + itemLayer.onUpdateDisplayPlaceholder(true, 0.0) } } @@ -1479,6 +1462,7 @@ public final class EmojiPagerContentComponent: Component { } } + var removedPlaceholerViews = false var removedIds: [ItemLayer.Key] = [] for (id, itemLayer) in self.visibleItemLayers { if !validIds.contains(id) { @@ -1491,6 +1475,7 @@ public final class EmojiPagerContentComponent: Component { if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) { view.removeFromSuperview() + removedPlaceholerViews = true } } @@ -1527,13 +1512,17 @@ public final class EmojiPagerContentComponent: Component { self.visibleGroupPremiumButtons.removeValue(forKey: id) } + if removedPlaceholerViews { + self.updateShimmerIfNeeded() + } + if let topVisibleGroupId = topVisibleGroupId { self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate)) } } private func updateShimmerIfNeeded() { - if self.visibleItemPlaceholderViews.isEmpty { + if self.placeholdersContainerView.subviews.isEmpty { self.standaloneShimmerEffect.layer = nil } else { self.standaloneShimmerEffect.layer = self.shimmerHostView.layer diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 33c1f5cbfc..e68b3ab752 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -76,8 +76,8 @@ public final class EntityKeyboardComponent: Component { public let theme: PresentationTheme public let bottomInset: CGFloat public let emojiContent: EmojiPagerContentComponent - public let stickerContent: EmojiPagerContentComponent - public let gifContent: GifPagerContentComponent + public let stickerContent: EmojiPagerContentComponent? + public let gifContent: GifPagerContentComponent? public let availableGifSearchEmojies: [GifSearchEmoji] public let defaultToEmojiTab: Bool public let externalTopPanelContainer: PagerExternalTopPanelContainer? @@ -94,8 +94,8 @@ public final class EntityKeyboardComponent: Component { theme: PresentationTheme, bottomInset: CGFloat, emojiContent: EmojiPagerContentComponent, - stickerContent: EmojiPagerContentComponent, - gifContent: GifPagerContentComponent, + stickerContent: EmojiPagerContentComponent?, + gifContent: GifPagerContentComponent?, availableGifSearchEmojies: [GifSearchEmoji], defaultToEmojiTab: Bool, externalTopPanelContainer: PagerExternalTopPanelContainer?, @@ -201,170 +201,179 @@ public final class EntityKeyboardComponent: Component { var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() - contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) - var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] - //TODO:localize - topGifItems.append(EntityKeyboardTopPanelComponent.Item( - id: "recent", - isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - imageName: "Chat/Input/Media/RecentTabIcon", - theme: component.theme, - title: "Recent", - pressed: { [weak self] in - self?.component?.switchToGifSubject(.recent) - } - )) - )) - topGifItems.append(EntityKeyboardTopPanelComponent.Item( - id: "trending", - isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - imageName: "Chat/Input/Media/TrendingGifs", - theme: component.theme, - title: "Trending", - pressed: { [weak self] in - self?.component?.switchToGifSubject(.trending) - } - )) - )) - for emoji in component.availableGifSearchEmojies { + let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() + + if transition.userData(MarkInputCollapsed.self) != nil { + self.searchComponent = nil + } + + if let gifContent = component.gifContent { + contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent))) + var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] + //TODO:localize topGifItems.append(EntityKeyboardTopPanelComponent.Item( - id: emoji.emoji, + id: "recent", isReorderable: false, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.stickerContent.context, - file: emoji.file, - animationCache: component.stickerContent.animationCache, - animationRenderer: component.stickerContent.animationRenderer, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: "Chat/Input/Media/RecentTabIcon", theme: component.theme, - title: emoji.title, + title: "Recent", pressed: { [weak self] in - self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) + self?.component?.switchToGifSubject(.recent) } )) )) - } - let defaultActiveGifItemId: AnyHashable - switch component.gifContent.subject { - case .recent: - defaultActiveGifItemId = "recent" - case .trending: - defaultActiveGifItemId = "trending" - case let .emojiSearch(value): - defaultActiveGifItemId = AnyHashable(value) - } - contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( - theme: component.theme, - items: topGifItems, - defaultActiveItemId: defaultActiveGifItemId, - activeContentItemIdUpdated: gifsContentItemIdUpdated, - reorderItems: { _ in + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: "trending", + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: "Chat/Input/Media/TrendingGifs", + theme: component.theme, + title: "Trending", + pressed: { [weak self] in + self?.component?.switchToGifSubject(.trending) + } + )) + )) + for emoji in component.availableGifSearchEmojies { + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: emoji.emoji, + isReorderable: false, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.emojiContent.context, + file: emoji.file, + animationCache: component.emojiContent.animationCache, + animationRenderer: component.emojiContent.animationRenderer, + theme: component.theme, + title: emoji.title, + pressed: { [weak self] in + self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) + } + )) + )) } - )))) - contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputGifsIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )))) - contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputSearchIcon", + let defaultActiveGifItemId: AnyHashable + switch gifContent.subject { + case .recent: + defaultActiveGifItemId = "recent" + case .trending: + defaultActiveGifItemId = "trending" + case let .emojiSearch(value): + defaultActiveGifItemId = AnyHashable(value) + } + contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topGifItems, + defaultActiveItemId: defaultActiveGifItemId, + activeContentItemIdUpdated: gifsContentItemIdUpdated, + reorderItems: { _ in + } + )))) + contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGifsIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil - )), - action: { [weak self] in - self?.openSearch() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) - - var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] - for itemGroup in component.stickerContent.itemGroups { - if let id = itemGroup.supergroupId.base as? String { - let iconMapping: [String: String] = [ - "saved": "Chat/Input/Media/SavedStickersTabIcon", - "recent": "Chat/Input/Media/RecentTabIcon", - "premium": "Chat/Input/Media/PremiumIcon" - ] - let titleMapping: [String: String] = [ - "saved": "Saved", - "recent": "Recent", - "premium": "Premium" - ] - if let iconName = iconMapping[id], let title = titleMapping[id] { - topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - imageName: iconName, - theme: component.theme, - title: title, - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) - } - )) - )) + )))) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSearchIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + self?.openSearch() } - } else { - if !itemGroup.items.isEmpty { - if let file = itemGroup.items[0].file { + ).minSize(CGSize(width: 38.0, height: 38.0))))) + } + + if let stickerContent = component.stickerContent { + var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in stickerContent.itemGroups { + if let id = itemGroup.supergroupId.base as? String { + let iconMapping: [String: String] = [ + "saved": "Chat/Input/Media/SavedStickersTabIcon", + "recent": "Chat/Input/Media/RecentTabIcon", + "premium": "Chat/Input/Media/PremiumIcon" + ] + let titleMapping: [String: String] = [ + "saved": "Saved", + "recent": "Recent", + "premium": "Premium" + ] + if let iconName = iconMapping[id], let title = titleMapping[id] { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, - isReorderable: true, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.stickerContent.context, - file: file, - animationCache: component.stickerContent.animationCache, - animationRenderer: component.stickerContent.animationRenderer, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: iconName, theme: component.theme, - title: itemGroup.title ?? "", + title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } + } else { + if !itemGroup.items.isEmpty { + if let file = itemGroup.items[0].file { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: true, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: stickerContent.context, + file: file, + animationCache: stickerContent.animationCache, + animationRenderer: stickerContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } } } + contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(stickerContent))) + contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topStickerItems, + activeContentItemIdUpdated: stickersContentItemIdUpdated, + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .stickers, items: items) + } + )))) + contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputStickersIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSearchIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + self?.openSearch() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + stickerContent.inputInteraction.openStickerSettings() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) } - let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() - contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent))) - contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( - theme: component.theme, - items: topStickerItems, - activeContentItemIdUpdated: stickersContentItemIdUpdated, - reorderItems: { [weak self] items in - guard let strongSelf = self else { - return - } - strongSelf.reorderPacks(category: .stickers, items: items) - } - )))) - contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputStickersIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )))) - contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputSearchIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { [weak self] in - self?.openSearch() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) - contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputSettingsIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { - component.stickerContent.inputInteraction.openStickerSettings() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) @@ -521,10 +530,6 @@ public final class EntityKeyboardComponent: Component { ) transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) - if transition.userData(MarkInputCollapsed.self) != nil { - self.searchComponent = nil - } - if let searchComponent = self.searchComponent { var animateIn = false let searchView: ComponentHostView @@ -546,7 +551,7 @@ public final class EntityKeyboardComponent: Component { component: AnyComponent(searchComponent), environment: { EntitySearchContentEnvironment( - context: component.stickerContent.context, + context: component.emojiContent.context, theme: component.theme, deviceMetrics: component.deviceMetrics ) @@ -669,7 +674,7 @@ public final class EntityKeyboardComponent: Component { case .emoji: namespace = Namespaces.ItemCollection.CloudEmojiPacks } - let _ = (component.stickerContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) + let _ = (component.emojiContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index ff57b95ec9..495748e37b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -313,7 +313,10 @@ final class EntityKeyboardBottomPanelComponent: Component { iconTotalSize.height = max(iconTotalSize.height, iconSize.height) } - var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0) + 2.0) + var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0)) + if component.bottomInset > 0.0 { + nextIconOrigin.y += 2.0 + } for icon in panelEnvironment.contentIcons { guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else { continue diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index c8ac47cfe5..7e2fe8382a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -100,7 +100,6 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { subgroupId: nil ), context: component.context, - groupId: "topPanel", attemptSynchronousLoad: false, file: component.file, staticEmoji: nil, @@ -110,18 +109,18 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { blurredBadgeColor: .clear, displayPremiumBadgeIfAvailable: false, pointSize: CGSize(width: 44.0, height: 44.0), - onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in guard let strongSelf = self else { return } - strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder) + strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder, duration: duration) } ) self.itemLayer = itemLayer self.layer.addSublayer(itemLayer) if itemLayer.displayPlaceholder { - self.updateDisplayPlaceholder(displayPlaceholder: true) + self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0) } } @@ -170,7 +169,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { return availableSize } - private func updateDisplayPlaceholder(displayPlaceholder: Bool) { + private func updateDisplayPlaceholder(displayPlaceholder: Bool, duration: Double) { if displayPlaceholder { if self.placeholderView == nil, let component = self.component { let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView( @@ -188,7 +187,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } else { if let placeholderView = self.placeholderView { self.placeholderView = nil - placeholderView.removeFromSuperview() + + if duration > 0.0 { + placeholderView.alpha = 0.0 + placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak placeholderView] _ in + placeholderView?.removeFromSuperview() + }) + } else { + placeholderView.removeFromSuperview() + } } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index b18ac64c11..a31a717d95 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -19,10 +19,11 @@ import SoftwareVideo import AVFoundation import PhotoResources import ContextUI +import ShimmerEffect private class GifVideoLayer: AVSampleBufferDisplayLayer { private let context: AccountContext - private let file: TelegramMediaFile + private let file: TelegramMediaFile? private var frameManager: SoftwareVideoLayerFrameManager? @@ -56,7 +57,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { } } - init(context: AccountContext, file: TelegramMediaFile, synchronousLoad: Bool) { + init(context: AccountContext, file: TelegramMediaFile?, synchronousLoad: Bool) { self.context = context self.file = file @@ -64,29 +65,31 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { self.videoGravity = .resizeAspectFill - if let dimensions = file.dimensions { - self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: self.file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) - |> deliverOnMainQueue).start(next: { [weak self] transform in - guard let strongSelf = self else { - return - } - let boundingSize = CGSize(width: 93.0, height: 93.0) - let imageSize = dimensions.cgSize.aspectFilled(boundingSize) - - if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.contents = image.cgImage - strongSelf.setupVideo() - strongSelf.started?() - } + if let file = self.file { + if let dimensions = file.dimensions { + self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) + |> deliverOnMainQueue).start(next: { [weak self] transform in + guard let strongSelf = self else { + return } - } else { - strongSelf.setupVideo() - } - }) - } else { - self.setupVideo() + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.contents = image.cgImage + strongSelf.setupVideo() + strongSelf.started?() + } + } + } else { + strongSelf.setupVideo() + } + }) + } else { + self.setupVideo() + } } } @@ -103,7 +106,10 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { } private func setupVideo() { - let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: self.file), layerHolder: nil, layer: self) + guard let file = self.file else { + return + } + let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: file), layerHolder: nil, layer: self) self.frameManager = frameManager frameManager.started = { [weak self] in guard let strongSelf = self else { @@ -127,13 +133,16 @@ public final class GifPagerContentComponent: Component { public final class InputInteraction { public let performItemAction: (Item, UIView, CGRect) -> Void public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void + public let loadMore: (String) -> Void public init( performItemAction: @escaping (Item, UIView, CGRect) -> Void, - openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void + openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void, + loadMore: @escaping (String) -> Void ) { self.performItemAction = performItemAction self.openGifContextMenu = openGifContextMenu + self.loadMore = loadMore } } @@ -160,17 +169,23 @@ public final class GifPagerContentComponent: Component { public let inputInteraction: InputInteraction public let subject: Subject public let items: [Item] + public let isLoading: Bool + public let loadMoreToken: String? public init( context: AccountContext, inputInteraction: InputInteraction, subject: Subject, - items: [Item] + items: [Item], + isLoading: Bool, + loadMoreToken: String? ) { self.context = context self.inputInteraction = inputInteraction self.subject = subject self.items = items + self.isLoading = isLoading + self.loadMoreToken = loadMoreToken } public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool { @@ -186,6 +201,12 @@ public final class GifPagerContentComponent: Component { if lhs.items != rhs.items { return false } + if lhs.isLoading != rhs.isLoading { + return false + } + if lhs.loadMoreToken != rhs.loadMoreToken { + return false + } return true } @@ -256,7 +277,7 @@ public final class GifPagerContentComponent: Component { let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) let minVisibleIndex = minVisibleRow * self.itemsPerRow - let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + let maxVisibleIndex = (maxVisibleRow + 1) * self.itemsPerRow - 1 if maxVisibleIndex >= minVisibleIndex { return minVisibleIndex ..< (maxVisibleIndex + 1) @@ -266,11 +287,14 @@ public final class GifPagerContentComponent: Component { } } + fileprivate enum ItemKey: Hashable { + case media(MediaId) + case placeholder(Int) + } + fileprivate final class ItemLayer: GifVideoLayer { - let item: Item + let item: Item? - private let file: TelegramMediaFile - private let placeholderColor: UIColor private var disposable: Disposable? private var fetchDisposable: Disposable? @@ -282,60 +306,32 @@ public final class GifPagerContentComponent: Component { } } } - private var displayPlaceholder: Bool = false + private(set) var displayPlaceholder: Bool = false { + didSet { + if self.displayPlaceholder != oldValue { + self.onUpdateDisplayPlaceholder(self.displayPlaceholder) + } + } + } + let onUpdateDisplayPlaceholder: (Bool) -> Void init( - item: Item, + item: Item?, context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, - file: TelegramMediaFile, - placeholderColor: UIColor + onUpdateDisplayPlaceholder: @escaping (Bool) -> Void ) { self.item = item - self.file = file - self.placeholderColor = placeholderColor + self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder - super.init(context: context, file: file, synchronousLoad: attemptSynchronousLoad) + super.init(context: context, file: item?.file, synchronousLoad: attemptSynchronousLoad) self.updateDisplayPlaceholder(displayPlaceholder: true) self.started = { [weak self] in self?.updateDisplayPlaceholder(displayPlaceholder: false) } - - /*if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { - self.displayPlaceholder = true - - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { - self.contents = image.cgImage - } - } - } - - self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } - - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() - return - } - cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) - }) - - let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - })*/ } required init?(coder: NSCoder) { @@ -364,17 +360,36 @@ public final class GifPagerContentComponent: Component { } func updateDisplayPlaceholder(displayPlaceholder: Bool) { - if self.displayPlaceholder == displayPlaceholder { - return - } - self.displayPlaceholder = displayPlaceholder + } + } + + final class ItemPlaceholderView: UIView { + private let shimmerView: PortalSourceView? + private var placeholderView: PortalView? + + init(shimmerView: PortalSourceView?) { + self.shimmerView = shimmerView + self.placeholderView = PortalView() - if displayPlaceholder { - let placeholderColor = self.placeholderColor - self.backgroundColor = placeholderColor.cgColor - } else { - self.backgroundColor = nil + super.init(frame: CGRect()) + + self.clipsToBounds = true + + if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView { + placeholderView.view.clipsToBounds = true + self.addSubview(placeholderView.view) + shimmerView.addPortal(view: placeholderView) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + if let placeholderView = self.placeholderView { + placeholderView.view.frame = CGRect(origin: CGPoint(), size: size) } } } @@ -382,9 +397,13 @@ public final class GifPagerContentComponent: Component { private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { } + private let shimmerHostView: PortalSourceView + private let standaloneShimmerEffect: StandaloneShimmerEffect + private let scrollView: ContentScrollView - private var visibleItemLayers: [MediaId: ItemLayer] = [:] + private var visibleItemPlaceholderViews: [ItemKey: ItemPlaceholderView] = [:] + private var visibleItemLayers: [ItemKey: ItemLayer] = [:] private var ignoreScrolling: Bool = false private var component: GifPagerContentComponent? @@ -392,11 +411,19 @@ public final class GifPagerContentComponent: Component { private var theme: PresentationTheme? private var itemLayout: ItemLayout? + private var currentLoadMoreToken: String? + override init(frame: CGRect) { + self.shimmerHostView = PortalSourceView() + self.standaloneShimmerEffect = StandaloneShimmerEffect() + self.scrollView = ContentScrollView() super.init(frame: frame) + self.shimmerHostView.alpha = 0.0 + self.addSubview(self.shimmerHostView) + self.scrollView.delaysContentTouches = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never @@ -450,7 +477,7 @@ public final class GifPagerContentComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { + if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[.media(item.file.fileId)] { component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) } } @@ -473,7 +500,11 @@ public final class GifPagerContentComponent: Component { for (_, itemLayer) in self.visibleItemLayers { if itemLayer.frame.contains(localPoint) { - return (itemLayer.item, itemLayer) + if let item = itemLayer.item { + return (item, itemLayer) + } else { + return nil + } } } @@ -497,6 +528,13 @@ public final class GifPagerContentComponent: Component { self.updateVisibleItems(attemptSynchronousLoads: false) self.updateScrollingOffset(transition: .immediate) + + if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height - 100.0 { + if let component = self.component, let loadMoreToken = component.loadMoreToken, self.currentLoadMoreToken != loadMoreToken { + self.currentLoadMoreToken = loadMoreToken + component.inputInteraction.loadMore(loadMoreToken) + } + } } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { @@ -564,44 +602,104 @@ public final class GifPagerContentComponent: Component { } private func updateVisibleItems(attemptSynchronousLoads: Bool) { - guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + guard let component = self.component, let itemLayout = self.itemLayout else { return } - var validIds = Set() + var validIds = Set() if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) { for index in itemRange.lowerBound ..< itemRange.upperBound { - let item = component.items[index] - let itemId = item.file.fileId + var item: Item? + let itemId: ItemKey + if index < component.items.count { + item = component.items[index] + itemId = .media(component.items[index].file.fileId) + } else if component.isLoading || component.loadMoreToken != nil { + itemId = .placeholder(index) + } else { + continue + } + validIds.insert(itemId) + let itemFrame = itemLayout.frame(at: index) + + let itemTransition: Transition = .immediate + var updateItemLayerPlaceholder = false + let itemLayer: ItemLayer if let current = self.visibleItemLayers[itemId] { itemLayer = current } else { + updateItemLayerPlaceholder = true + itemLayer = ItemLayer( item: item, context: component.context, groupId: "savedGif", attemptSynchronousLoad: attemptSynchronousLoads, - file: item.file, - placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + guard let strongSelf = self else { + return + } + if displayPlaceholder { + if let itemLayer = strongSelf.visibleItemLayers[itemId] { + let placeholderView: ItemPlaceholderView + if let current = strongSelf.visibleItemPlaceholderViews[itemId] { + placeholderView = current + } else { + placeholderView = ItemPlaceholderView(shimmerView: strongSelf.shimmerHostView) + strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView + strongSelf.scrollView.insertSubview(placeholderView, at: 0) + } + placeholderView.frame = itemLayer.frame + placeholderView.update(size: placeholderView.bounds.size) + + strongSelf.updateShimmerIfNeeded() + } + } else { + if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { + strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) + placeholderView.removeFromSuperview() + + strongSelf.updateShimmerIfNeeded() + } + } + } ) self.scrollView.layer.addSublayer(itemLayer) self.visibleItemLayers[itemId] = itemLayer } - itemLayer.frame = itemLayout.frame(at: index) + let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) + + itemTransition.setFrame(layer: itemLayer, frame: itemFrame) itemLayer.isVisibleForAnimations = true + + if let placeholderView = self.visibleItemPlaceholderViews[itemId] { + if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds { + itemTransition.setFrame(view: placeholderView, frame: itemFrame) + placeholderView.update(size: itemFrame.size) + } + } else if updateItemLayerPlaceholder { + if itemLayer.displayPlaceholder { + itemLayer.onUpdateDisplayPlaceholder(true) + } + } } } - var removedIds: [MediaId] = [] + var removedIds: [ItemKey] = [] for (id, itemLayer) in self.visibleItemLayers { if !validIds.contains(id) { removedIds.append(id) itemLayer.removeFromSuperlayer() + + if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) { + view.removeFromSuperview() + } } } for id in removedIds { @@ -609,13 +707,35 @@ public final class GifPagerContentComponent: Component { } } + private func updateShimmerIfNeeded() { + if self.visibleItemPlaceholderViews.isEmpty { + self.standaloneShimmerEffect.layer = nil + } else { + self.standaloneShimmerEffect.layer = self.shimmerHostView.layer + } + } + func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var contentReset = false + if let previousComponent = self.component, previousComponent.subject != component.subject { + contentReset = true + self.currentLoadMoreToken = nil + } + + let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value + self.component = component - self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme + self.theme = keyboardChildEnvironment.theme let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value self.pagerEnvironment = pagerEnvironment + transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08) + let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) + self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) + let itemLayout = ItemLayout( width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right), @@ -631,6 +751,11 @@ public final class GifPagerContentComponent: Component { if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets } + + if contentReset { + self.scrollView.setContentOffset(CGPoint(), animated: false) + } + self.previousScrollingOffset = self.scrollView.contentOffset.y self.ignoreScrolling = false diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 4237c59b90..2a1c71affb 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -297,11 +297,11 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { var targets: [TargetReference] = [] var slotIndex: Int - private let preferredBytesPerRow: Int + private let preferredRowAlignment: Int - init(slotIndex: Int, preferredBytesPerRow: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { + init(slotIndex: Int, preferredRowAlignment: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { self.slotIndex = slotIndex - self.preferredBytesPerRow = preferredBytesPerRow + self.preferredRowAlignment = preferredRowAlignment self.cache = cache self.stateUpdated = stateUpdated @@ -375,10 +375,10 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { let readyTextureU = texturePoolHalfPlane.take() let readyTextureV = texturePoolHalfPlane.take() let readyTextureA = texturePoolFullPlane.take() - let preferredBytesPerRow = self.preferredBytesPerRow + let preferredRowAlignment = self.preferredRowAlignment return LoadFrameTask(task: { [weak self] in - let frame = item.getFrame(at: timestamp, requestedFormat: .yuva(bytesPerRow: preferredBytesPerRow)) + let frame = item.getFrame(at: timestamp, requestedFormat: .yuva(rowAlignment: preferredRowAlignment)) let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) @@ -419,7 +419,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { private let texturePoolFullPlane: TextureStoragePool private let texturePoolHalfPlane: TextureStoragePool - private let preferredBytesPerRow: Int + private let preferredRowAlignment: Int private let slotCount: Int private let slotsX: Int @@ -468,7 +468,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { self.texturePoolFullPlane = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height), format: .r) self.texturePoolHalfPlane = TextureStoragePool(width: Int(self.cellSize.width) / 2, height: Int(self.cellSize.height) / 2, format: .r) - self.preferredBytesPerRow = alignUp(size: Int(self.cellSize.width), align: TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r)) + self.preferredRowAlignment = TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r) super.init() @@ -508,7 +508,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { for i in 0 ..< self.slotCount { if self.slotToItemId[i] == nil { self.slotToItemId[i] = itemId - self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredBytesPerRow: self.preferredBytesPerRow, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in + self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredRowAlignment: self.preferredRowAlignment, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -778,7 +778,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { self.isPlaying = isPlaying } - public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { assert(Thread.isMainThread) let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16))) @@ -805,11 +805,11 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { } } - public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { return false } - public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { completion(false) return EmptyDisposable diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index c30fa0affe..08a52b35db 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -6,9 +6,9 @@ import AnimationCache import Accelerate public protocol MultiAnimationRenderer: AnyObject { - func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable - func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool - func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool + func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable } private var nextRenderTargetId: Int64 = 1 @@ -60,6 +60,9 @@ open class MultiAnimationRenderTarget: SimpleLayer { open func updateDisplayPlaceholder(displayPlaceholder: Bool) { } + + open func transitionToContents(_ contents: AnyObject) { + } } private final class FrameGroup { @@ -178,8 +181,9 @@ private final class ItemAnimationContext { func updateAddedTarget(target: MultiAnimationRenderTarget) { if let currentFrameGroup = self.currentFrameGroup { - target.updateDisplayPlaceholder(displayPlaceholder: false) - target.contents = currentFrameGroup.image.cgImage + if let cgImage = currentFrameGroup.image.cgImage { + target.transitionToContents(cgImage) + } } self.updateIsPlaying() @@ -239,8 +243,7 @@ private final class ItemAnimationContext { strongSelf.currentFrameGroup = currentFrameGroup for target in strongSelf.targets.copyItems() { if let target = target.value { - target.contents = currentFrameGroup.image.cgImage - target.updateDisplayPlaceholder(displayPlaceholder: false) + target.transitionToContents(currentFrameGroup.image.cgImage!) } } } @@ -400,7 +403,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) - private var groupContexts: [String: GroupContext] = [:] + private var groupContext: GroupContext? private var frameSkip: Int private var displayLink: ConstantDisplayLinkAnimator? @@ -436,9 +439,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { let groupContext: GroupContext - if let current = self.groupContexts[groupId] { + if let current = self.groupContext { groupContext = current } else { groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in @@ -447,7 +450,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.updateIsPlaying() }) - self.groupContexts[groupId] = groupContext + self.groupContext = groupContext } let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) @@ -457,9 +460,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { let groupContext: GroupContext - if let current = self.groupContexts[groupId] { + if let current = self.groupContext { groupContext = current } else { groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in @@ -468,15 +471,15 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.updateIsPlaying() }) - self.groupContexts[groupId] = groupContext + self.groupContext = groupContext } return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) } - public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { let groupContext: GroupContext - if let current = self.groupContexts[groupId] { + if let current = self.groupContext { groupContext = current } else { groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in @@ -485,7 +488,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.updateIsPlaying() }) - self.groupContexts[groupId] = groupContext + self.groupContext = groupContext } return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion) @@ -493,10 +496,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { private func updateIsPlaying() { var isPlaying = false - for (_, groupContext) in self.groupContexts { + if let groupContext = self.groupContext { if groupContext.isPlaying { isPlaying = true - break } } @@ -507,7 +509,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { let secondsPerFrame = Double(self.frameSkip) / 60.0 var tasks: [LoadFrameGroupTask] = [] - for (_, groupContext) in self.groupContexts { + if let groupContext = self.groupContext { if groupContext.isPlaying { tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) } diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index d0809698b0..e8fdca3051 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -39,6 +39,18 @@ private final class InlineStickerItem: Hashable { } } +private final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } +} + public final class TextNodeWithEntities { public final class Arguments { public let context: AccountContext @@ -105,6 +117,35 @@ public final class TextNodeWithEntities { if let value = value as? ChatTextInputTextCustomEmojiAttribute { if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range) + + let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(range.length) + + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: itemSize + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateVersion1, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + string.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: range) + } } } }) @@ -168,7 +209,7 @@ public final class TextNodeWithEntities { if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) @@ -331,7 +372,7 @@ public class ImmediateTextNodeWithEntities: TextNode { if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fae7867aee..b578b8dcf9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1031,7 +1031,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var insets: UIEdgeInsets var inputPanelBottomInsetTerm: CGFloat = 0.0 - if inputNodeForState != nil { + if let inputNodeForState = inputNodeForState { + if !self.inputPanelContainerNode.stableIsExpanded && inputNodeForState.adjustLayoutForHiddenInput { + inputNodeForState.hideInput = false + inputNodeForState.adjustLayoutForHiddenInput = false + } + insets = layout.insets(options: []) inputPanelBottomInsetTerm = max(insets.bottom, layout.standardInputHeight) } else { @@ -1191,9 +1196,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNode.hideInputUpdated = { [weak self] transition in self?.updateInputPanelBackgroundExpansion(transition: transition) } - inputNode.expansionFractionUpdated = { [weak self] transition in - self?.updateInputPanelBackgroundExpansion(transition: transition) - } dismissedInputNode = self.inputNode if let inputNode = self.inputNode { @@ -1236,7 +1238,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNodeHeightAndOverflow = ( boundedHeight, - max(0.0, inputHeight - boundedHeight) + inputNode.followsDefaultHeight ? max(0.0, inputHeight - boundedHeight) : 0.0 ) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode @@ -2058,7 +2060,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) - let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) + var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) + if self.chatPresentationInterfaceStateInputView(self.chatPresentationInterfaceState) !== self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) { + updatedInputFocus = true + } let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState @@ -2171,18 +2176,22 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated) } + var waitForKeyboardLayout = false if let textView = self.textInputPanelNode?.textInputNode?.textView { let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) if textView.inputView !== updatedInputView { textView.inputView = updatedInputView if textView.isFirstResponder { + if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) { + waitForKeyboardLayout = true + } textView.reloadInputViews() } } } if updatedInputFocus { - if !self.ignoreUpdateHeight { + if !self.ignoreUpdateHeight && !waitForKeyboardLayout { self.scheduleLayoutTransitionRequest(layoutTransition) } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 64aa27d3e5..0def32c9a9 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -238,16 +238,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }, chatPeerId: chatPeerId ) - let gifInputInteraction = GifPagerContentComponent.InputInteraction( - performItemAction: { [weak controllerInteraction] item, view, rect in - guard let controllerInteraction = controllerInteraction else { - return - } - let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) - }, - openGifContextMenu: { _, _, _, _, _ in - } - ) let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path @@ -560,22 +550,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return animatedEmojiStickers } - // We are intentionally not subscribing to the recent gif updates here - let gifItems: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) - |> map { savedGifs -> GifPagerContentComponent in - var items: [GifPagerContentComponent.Item] = [] - for gifItem in savedGifs { - items.append(GifPagerContentComponent.Item( - file: gifItem.contents.get(RecentMediaItem.self)!.media - )) + let gifInputInteraction = GifPagerContentComponent.InputInteraction( + performItemAction: { [weak controllerInteraction] item, view, rect in + guard let controllerInteraction = controllerInteraction else { + return + } + let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + }, + openGifContextMenu: { _, _, _, _, _ in + }, + loadMore: { _ in } - return GifPagerContentComponent( - context: context, - inputInteraction: gifInputInteraction, - subject: .recent, - items: items - ) - } + ) + + // We are going to subscribe to the actual data when the view is loaded + let gifItems: Signal = .single(GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: .recent, + items: [], + isLoading: false, + loadMoreToken: nil + )) return combineLatest(queue: .mainQueue(), emojiItems, @@ -625,10 +621,195 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private var gifMode: GifPagerContentComponent.Subject = .recent { didSet { - self.gifModeSubject.set(self.gifMode) + if self.gifMode != oldValue { + self.reloadGifContext() + } } } - private let gifModeSubject: ValuePromise + + private final class GifContext { + private var componentValue: GifPagerContentComponent? { + didSet { + if let componentValue = self.componentValue { + self.componentResult.set(.single(componentValue)) + } + } + } + private let componentPromise = Promise() + + private let componentResult = Promise() + var component: Signal { + 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) { + self.context = context + self.subject = subject + self.gifInputInteraction = gifInputInteraction + + let gifItems: Signal + 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 + 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() + 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() + private var gifInputInteraction: GifPagerContentComponent.InputInteraction? init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) { self.context = context @@ -639,17 +820,18 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.entityKeyboardView = ComponentHostView() - self.gifModeSubject = ValuePromise(self.gifMode, ignoreRepeated: true) - super.init() + self.topBackgroundExtension = 41.0 + self.followsDefaultHeight = true + self.view.addSubview(self.entityKeyboardView) self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, - self.updatedGifs() + self.gifComponent.get() ) |> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in guard let strongSelf = self else { @@ -685,6 +867,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } ) + self.trendingGifsPromise.set(.single(nil)) self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { items -> ChatMediaInputGifPaneTrendingState? in if let items = items { @@ -693,14 +876,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return nil } }) - } - - deinit { - self.inputDataDisposable?.dispose() - } - - private func updatedGifs() -> Signal { - let gifInputInteraction = GifPagerContentComponent.InputInteraction( + + self.gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { [weak controllerInteraction] item, view, rect in guard let controllerInteraction = controllerInteraction else { return @@ -712,83 +889,26 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return } strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + }, + loadMore: { [weak self] token in + guard let strongSelf = self, let gifContext = strongSelf.gifContext else { + return + } + gifContext.loadMore(token: token) } ) - let context = self.context - let trendingGifs = self.trendingGifsPromise.get() - let updatedGifs = self.gifModeSubject.get() - |> mapToSignal { subject -> Signal in - switch subject { - case .recent: - let gifItems: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) - |> map { savedGifs -> GifPagerContentComponent in - var items: [GifPagerContentComponent.Item] = [] - for gifItem in savedGifs { - items.append(GifPagerContentComponent.Item( - file: gifItem.contents.get(RecentMediaItem.self)!.media - )) - } - return GifPagerContentComponent( - context: context, - inputInteraction: gifInputInteraction, - subject: subject, - items: items - ) - } - return gifItems - case .trending: - return trendingGifs - |> map { trendingGifs -> GifPagerContentComponent in - var items: [GifPagerContentComponent.Item] = [] - - if let trendingGifs = trendingGifs { - for file in trendingGifs.files { - items.append(GifPagerContentComponent.Item( - file: file.file.media - )) - } - } - - return GifPagerContentComponent( - context: context, - inputInteraction: gifInputInteraction, - subject: subject, - items: items - ) - } - case let .emojiSearch(query): - return paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil) - |> map { result -> GifPagerContentComponent in - var items: [GifPagerContentComponent.Item] = [] - - /*let canLoadMore: Bool - if let result = result { - canLoadMore = !result.isComplete - } else { - canLoadMore = true - }*/ - - if let result = result { - for file in result.files { - items.append(GifPagerContentComponent.Item( - file: file.file.media - )) - } - } - - return GifPagerContentComponent( - context: context, - inputInteraction: gifInputInteraction, - subject: subject, - items: items - ) - } - } + self.reloadGifContext() + } + + deinit { + self.inputDataDisposable?.dispose() + } + + private func reloadGifContext() { + if let gifInputInteraction = self.gifInputInteraction { + self.gifContext = GifContext(context: self.context, subject: self.gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get()) } - - return .single(self.currentInputData.gifs) - |> then(updatedGifs) } func markInputCollapsed() { @@ -808,7 +928,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let wasMarkedInputCollapsed = self.isMarkInputCollapsed self.isMarkInputCollapsed = false - let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight) + let expandedHeight = standardInputHeight var hiddenInputHeight: CGFloat = 0.0 if self.hideInput && !self.adjustLayoutForHiddenInput { @@ -822,18 +942,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var mappedTransition = Transition(transition) - if wasMarkedInputCollapsed { + if wasMarkedInputCollapsed || !isExpanded { mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) } + var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers + var gifContent: GifPagerContentComponent? = self.currentInputData.gifs + + var stickersEnabled = true + if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { + if peer.hasBannedPermission(.banSendStickers) != nil { + stickersEnabled = false + } + } else if let peer = interfaceState.renderedPeer?.peer as? TelegramGroup { + if peer.hasBannedPermission(.banSendStickers) { + stickersEnabled = false + } + } + + if !stickersEnabled || interfaceState.interfaceState.editMessage != nil { + stickerContent = nil + gifContent = nil + } + let entityKeyboardSize = self.entityKeyboardView.update( transition: mappedTransition, component: AnyComponent(EntityKeyboardComponent( theme: interfaceState.theme, bottomInset: bottomInset, emojiContent: self.currentInputData.emoji, - stickerContent: self.currentInputData.stickers, - gifContent: self.currentInputData.gifs, + stickerContent: stickerContent, + gifContent: gifContent, availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies, defaultToEmojiTab: self.defaultToEmojiTab, externalTopPanelContainer: self.externalTopPanelContainerImpl, @@ -868,7 +1007,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - strongSelf.gifModeSubject.set(subject) + strongSelf.gifMode = subject }, makeSearchContainerNode: { content in let mappedMode: ChatMediaInputSearchMode diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift index 311f6a0a8d..1aa38814f0 100644 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputNode.swift @@ -15,15 +15,14 @@ class ChatInputNode: ASDisplayNode { return nil } - var topBackgroundExtension: CGFloat = 41.0 + var topBackgroundExtension: CGFloat = 0.0 var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? var hideInput: Bool = false var adjustLayoutForHiddenInput: Bool = false var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)? - var expansionFraction: CGFloat = 0.0 - var expansionFractionUpdated: ((ContainedViewLayoutTransition) -> Void)? + var followsDefaultHeight: Bool = false func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { return (0.0, 0.0) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 3e8289094b..7c88fd6fb8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -272,15 +272,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte case .inputButtons: return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) case .none, .text: - if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { - let isTextEmpty = editMessage.inputState.inputText.length == 0 - - let stickersAreEmoji = !isTextEmpty - - var stickersEnabled = true - stickersEnabled = true - - accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji)) + if let _ = chatPresentationInterfaceState.interfaceState.editMessage { + accessoryItems.append(.stickers(isEnabled: true, isEmoji: true)) return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } else { @@ -330,7 +323,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte accessoryItems.append(.commands) } - accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji)) + if stickersEnabled { + accessoryItems.append(.stickers(isEnabled: true, isEmoji: stickersAreEmoji)) + } else { + accessoryItems.append(.stickers(isEnabled: true, isEmoji: true)) + } if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 6d1a5550fe..2df9ca3c88 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -49,7 +49,7 @@ private final class CachedChatMessageText { class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNodeWithEntities - private var spoilerTextNode: TextNode? + private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode @@ -67,11 +67,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { switch self.visibility { case .none: self.textNode.visibilityRect = nil + self.spoilerTextNode?.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 self.textNode.visibilityRect = subRect + self.spoilerTextNode?.visibilityRect = subRect } } } @@ -120,7 +122,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) - let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode) + let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) let statusLayout = self.statusNode.asyncLayout() let currentCachedChatMessageText = self.cachedChatMessageText @@ -339,9 +341,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor)) - let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? if !textLayout.spoilers.isEmpty { - spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true)) + spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) } else { spoilerTextLayoutAndApply = nil } @@ -440,33 +442,33 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { - let spoilerTextNode = spoilerTextApply() + let spoilerTextNode = spoilerTextApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads)) if strongSelf.spoilerTextNode == nil { - spoilerTextNode.alpha = 0.0 - spoilerTextNode.isUserInteractionEnabled = false - spoilerTextNode.contentMode = .topLeft - spoilerTextNode.contentsScale = UIScreenScale - spoilerTextNode.displaysAsynchronously = false - strongSelf.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) + spoilerTextNode.textNode.alpha = 0.0 + spoilerTextNode.textNode.isUserInteractionEnabled = false + spoilerTextNode.textNode.contentMode = .topLeft + spoilerTextNode.textNode.contentsScale = UIScreenScale + spoilerTextNode.textNode.displaysAsynchronously = false + strongSelf.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) strongSelf.spoilerTextNode = spoilerTextNode } - strongSelf.spoilerTextNode?.frame = textFrame + strongSelf.spoilerTextNode?.textNode.frame = textFrame let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: spoilerTextNode) + dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode) strongSelf.dustNode = dustNode - strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) + strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, textColor: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil - spoilerTextNode.removeFromSupernode() + spoilerTextNode.textNode.removeFromSupernode() if let dustNode = strongSelf.dustNode { strongSelf.dustNode = nil @@ -474,6 +476,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + switch strongSelf.visibility { + case .none: + strongSelf.textNode.visibilityRect = nil + strongSelf.spoilerTextNode?.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.textNode.visibilityRect = subRect + strongSelf.spoilerTextNode?.visibilityRect = subRect + } + if let textSelectionNode = strongSelf.textSelectionNode { let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size textSelectionNode.frame = textFrame diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index cfb4bb135a..c07d3aa24c 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -56,7 +56,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNodeWithEntities - private var spoilerTextNode: TextNode? + private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private let actionButton: HighlightableButtonNode private let actionButtonTitleNode: ImmediateTextNode @@ -525,7 +525,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let makeTitleLayout = self.titleNode.asyncLayout() let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) - let makeSpoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode) + let makeSpoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) let imageNodeLayout = self.imageNode.asyncLayout() let previousMediaReference = self.previousMediaReference @@ -668,9 +668,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) - let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? if !textLayout.spoilers.isEmpty { - spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true)) + spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) } else { spoilerTextLayoutAndApply = nil } @@ -701,33 +701,33 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { strongSelf.textNode.textNode.frame = textFrame if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { - let spoilerTextNode = spoilerTextApply() + let spoilerTextNode = spoilerTextApply(textArguments) if strongSelf.spoilerTextNode == nil { - spoilerTextNode.alpha = 0.0 - spoilerTextNode.isUserInteractionEnabled = false - spoilerTextNode.contentMode = .topLeft - spoilerTextNode.contentsScale = UIScreenScale - spoilerTextNode.displaysAsynchronously = false - strongSelf.contentTextContainer.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textNode.textNode) + spoilerTextNode.textNode.alpha = 0.0 + spoilerTextNode.textNode.isUserInteractionEnabled = false + spoilerTextNode.textNode.contentMode = .topLeft + spoilerTextNode.textNode.contentsScale = UIScreenScale + spoilerTextNode.textNode.displaysAsynchronously = false + strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode) strongSelf.spoilerTextNode = spoilerTextNode } - strongSelf.spoilerTextNode?.frame = textFrame + strongSelf.spoilerTextNode?.textNode.frame = textFrame let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: spoilerTextNode) + dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode) strongSelf.dustNode = dustNode - strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) + strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil - spoilerTextNode.removeFromSupernode() + spoilerTextNode.textNode.removeFromSupernode() if let dustNode = strongSelf.dustNode { strongSelf.dustNode = nil @@ -735,6 +735,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } } + strongSelf.textNode.visibilityRect = CGRect.infinite + strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite + let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) strongSelf.lineNode.update( diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index f7de318cab..4dde3ce13c 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -619,10 +619,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.menuButton.cornerRadius = 16.0 self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu self.menuButtonBackgroundNode = ASDisplayNode() + self.menuButtonBackgroundNode.isUserInteractionEnabled = false self.menuButtonClippingNode = ASDisplayNode() self.menuButtonClippingNode.clipsToBounds = true + self.menuButtonClippingNode.isUserInteractionEnabled = false self.menuButtonIconNode = MenuIconNode() + self.menuButtonIconNode.isUserInteractionEnabled = false self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor self.menuButtonTextNode = ImmediateTextNode() @@ -1782,6 +1785,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) if let textInputNode = self.textInputNode { + textInputNode.textContainerInset = textInputViewRealInsets let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size transition.updateFrame(node: textInputNode, frame: textFieldFrame) @@ -2069,7 +2073,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let endIndex = currentIndex addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) } - } else if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + } + + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) { let textRects = textInputNode.textView.selectionRects(for: textRange) for textRect in textRects { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 57ddcbac1b..73927f1538 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -126,11 +126,11 @@ public final class TelegramRootController: NavigationController { } let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true) - accountSettingsController.tabBarItemDebugTapAction = { [weak self, weak accountSettingsController] in - guard let strongSelf = self, let accountSettingsController = accountSettingsController else { + accountSettingsController.tabBarItemDebugTapAction = { [weak self] in + guard let strongSelf = self else { return } - accountSettingsController.push(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context)) + strongSelf.pushViewController(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context)) } controllers.append(accountSettingsController) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 8d3924042d..6d81e65b0c 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -139,35 +139,6 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo } }) - /*if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider { - let _ = CustomTextAttachmentViewProvider.ensureRegistered - - var nextIndex: [String: Int] = [:] - - result.string.enumerateSubstrings(in: result.string.startIndex ..< result.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in - if let substring = substring { - let emoji = substring.basicEmoji.0 - - if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) { - let index: Int - if let value = nextIndex[emoji] { - index = value - } else { - index = 0 - } - nextIndex[emoji] = index + 1 - - let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider) - attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0)) - - result.replaceCharacters(in: NSRange(substringRange, in: result.string), with: NSAttributedString(attachment: attachment)) - - stop = true - } - } - } - }*/ - return result } @@ -562,15 +533,6 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { textNode.textView.textStorage.addAttribute(key, value: value, range: range) - if let emojiViewProvider = emojiViewProvider { - let _ = emojiViewProvider - /*let emojiText = attributedText.attributedSubstring(from: range) - let attachment = EmojiTextAttachment(index: emojiIndex, text: emojiText.string, emoji: value, viewProvider: emojiViewProvider) - emojiIndex += 1 - attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0)) - - replaceRanges.append((range, attachment))*/ - } } } @@ -602,52 +564,6 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme textNode.textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment)) } } - - if #available(iOS 15, *), let _ = emojiViewProvider { - let _ = CustomTextAttachmentViewProvider.ensureRegistered - - /*var nextIndex: [String: Int] = [:] - - var count = 0 - - let fullRange = NSRange(textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, in: textNode.textView.textStorage.string) - textNode.textView.textStorage.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange, options: [], using: { value, _, _ in - if let _ = value as? EmojiTextAttachment { - count += 1 - } - }) - - while count < 400 { - var found = false - textNode.textView.textStorage.string.enumerateSubstrings(in: textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in - if let substring = substring { - let emoji = substring.basicEmoji.0 - - if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) { - let index: Int - if let value = nextIndex[emoji] { - index = value - } else { - index = 0 - } - nextIndex[emoji] = index + 1 - - let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider) - attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0)) - - textNode.textView.textStorage.replaceCharacters(in: NSRange(substringRange, in: textNode.textView.textStorage.string), with: NSAttributedString(attachment: attachment)) - - count += 1 - found = true - stop = true - } - } - } - if !found { - break - } - }*/ - } } public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) {