mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
38b6555518
39 changed files with 1363 additions and 238 deletions
|
|
@ -44,6 +44,7 @@ public protocol UniversalVideoContentNode: AnyObject {
|
|||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool)
|
||||
func enterNativePictureInPicture() -> Bool
|
||||
func exitNativePictureInPicture()
|
||||
func setNativePictureInPictureIsActive(_ value: Bool)
|
||||
}
|
||||
|
||||
public protocol UniversalVideoContent {
|
||||
|
|
@ -445,4 +446,12 @@ public final class UniversalVideoNode: ASDisplayNode {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||
if let contentNode = contentNode {
|
||||
contentNode.setNativePictureInPictureIsActive(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -748,11 +748,15 @@ public struct ComponentTransition {
|
|||
}
|
||||
|
||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animateScale(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
layer.animate(
|
||||
from: fromValue as NSNumber,
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
|
|
|
|||
|
|
@ -476,6 +476,7 @@ public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloa
|
|||
|
||||
public enum DrawingContextBltMode {
|
||||
case Alpha
|
||||
case AlphaFromColor
|
||||
}
|
||||
|
||||
public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings {
|
||||
|
|
@ -745,38 +746,64 @@ public class DrawingContext {
|
|||
let maxDstY = dstY + height
|
||||
|
||||
switch mode {
|
||||
case .Alpha:
|
||||
while dstY < maxDstY {
|
||||
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
case .Alpha:
|
||||
while dstY < maxDstY {
|
||||
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
var dx = dstX
|
||||
var sx = srcX
|
||||
while dx < maxDstX {
|
||||
let srcPixel = srcLine + sx
|
||||
let dstPixel = dstLine + dx
|
||||
|
||||
var dx = dstX
|
||||
var sx = srcX
|
||||
while dx < maxDstX {
|
||||
let srcPixel = srcLine + sx
|
||||
let dstPixel = dstLine + dx
|
||||
|
||||
let baseColor = dstPixel.pointee
|
||||
let baseAlpha = (baseColor >> 24) & 0xff
|
||||
let baseR = (baseColor >> 16) & 0xff
|
||||
let baseG = (baseColor >> 8) & 0xff
|
||||
let baseB = baseColor & 0xff
|
||||
|
||||
let alpha = min(baseAlpha, srcPixel.pointee >> 24)
|
||||
|
||||
let r = (baseR * alpha) / 255
|
||||
let g = (baseG * alpha) / 255
|
||||
let b = (baseB * alpha) / 255
|
||||
|
||||
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
|
||||
|
||||
dx += 1
|
||||
sx += 1
|
||||
}
|
||||
let baseColor = dstPixel.pointee
|
||||
let baseAlpha = (baseColor >> 24) & 0xff
|
||||
let baseR = (baseColor >> 16) & 0xff
|
||||
let baseG = (baseColor >> 8) & 0xff
|
||||
let baseB = baseColor & 0xff
|
||||
|
||||
dstY += 1
|
||||
srcY += 1
|
||||
let alpha = min(baseAlpha, srcPixel.pointee >> 24)
|
||||
|
||||
let r = (baseR * alpha) / 255
|
||||
let g = (baseG * alpha) / 255
|
||||
let b = (baseB * alpha) / 255
|
||||
|
||||
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
|
||||
|
||||
dx += 1
|
||||
sx += 1
|
||||
}
|
||||
|
||||
dstY += 1
|
||||
srcY += 1
|
||||
}
|
||||
case .AlphaFromColor:
|
||||
while dstY < maxDstY {
|
||||
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
var dx = dstX
|
||||
var sx = srcX
|
||||
while dx < maxDstX {
|
||||
let srcPixel = srcLine + sx
|
||||
let dstPixel = dstLine + dx
|
||||
|
||||
let alpha = (srcPixel.pointee >> 0) & 0xff
|
||||
|
||||
let r = alpha
|
||||
let g = alpha
|
||||
let b = alpha
|
||||
|
||||
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
|
||||
|
||||
dx += 1
|
||||
sx += 1
|
||||
}
|
||||
|
||||
dstY += 1
|
||||
srcY += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,15 +66,23 @@ public enum SourceAndRect {
|
|||
case node(() -> (ASDisplayNode, CGRect)?)
|
||||
case view(() -> (UIView, CGRect)?)
|
||||
|
||||
func globalRect() -> CGRect? {
|
||||
func globalRect(isAlreadyGlobal: Bool) -> CGRect? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
if let (sourceNode, sourceRect) = node() {
|
||||
return sourceNode.view.convert(sourceRect, to: nil)
|
||||
if isAlreadyGlobal {
|
||||
return sourceRect
|
||||
} else {
|
||||
return sourceNode.view.convert(sourceRect, to: nil)
|
||||
}
|
||||
}
|
||||
case let .view(view):
|
||||
if let (sourceView, sourceRect) = view() {
|
||||
return sourceView.convert(sourceRect, to: nil)
|
||||
if isAlreadyGlobal {
|
||||
return sourceRect
|
||||
} else {
|
||||
return sourceView.convert(sourceRect, to: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -83,13 +91,16 @@ public enum SourceAndRect {
|
|||
|
||||
public final class TooltipControllerPresentationArguments {
|
||||
public let sourceAndRect: SourceAndRect
|
||||
public let sourceRectIsGlobal: Bool
|
||||
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?) {
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?, sourceRectIsGlobal: Bool = false) {
|
||||
self.sourceAndRect = .node(sourceNodeAndRect)
|
||||
self.sourceRectIsGlobal = sourceRectIsGlobal
|
||||
}
|
||||
|
||||
public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?) {
|
||||
public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?, sourceRectIsGlobal: Bool = false) {
|
||||
self.sourceAndRect = .view(sourceViewAndRect)
|
||||
self.sourceRectIsGlobal = sourceRectIsGlobal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +205,7 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
|||
} else {
|
||||
self.layout = layout
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect() {
|
||||
if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect(isAlreadyGlobal: presentationArguments.sourceRectIsGlobal) {
|
||||
self.controllerNode.sourceRect = sourceRect
|
||||
} else {
|
||||
self.controllerNode.sourceRect = nil
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||
"//submodules/UrlHandling",
|
||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||
"//submodules/TelegramUI/Components/RasterizedCompositionComponent",
|
||||
"//submodules/TelegramUI/Components/BadgeComponent",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -957,7 +957,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
} else if media is TelegramMediaImage {
|
||||
hasCaption = true
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
hasCaption = file.mimeType.hasPrefix("image/")
|
||||
hasCaption = file.mimeType.hasPrefix("image/") || file.mimeType.hasPrefix("video/")
|
||||
} else if media is TelegramMediaInvoice {
|
||||
hasCaption = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,6 +470,10 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
|||
let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0
|
||||
let minimalDismissDistance = scrollView.contentSize.height / 12.0
|
||||
if abs(velocity.y) > 1.0 || abs(distanceFromEquilibrium) > minimalDismissDistance {
|
||||
if distanceFromEquilibrium > 1.0, let centralItemNode = self.pager.centralItemNode(), centralItemNode.maybePerformActionForSwipeDismiss() {
|
||||
return
|
||||
}
|
||||
|
||||
if let backgroundColor = self.backgroundNode.backgroundColor {
|
||||
self.backgroundNode.layer.animate(from: backgroundColor, to: UIColor(white: 0.0, alpha: 0.0).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ open class GalleryItemNode: ASDisplayNode {
|
|||
open func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
|
||||
}
|
||||
|
||||
open func maybePerformActionForSwipeDismiss() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func contentSize() -> CGSize? {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ import AdsInfoScreen
|
|||
import AdsReportScreen
|
||||
import SaveProgressScreen
|
||||
import SectionTitleContextItem
|
||||
import RasterizedCompositionComponent
|
||||
import BadgeComponent
|
||||
import ComponentFlow
|
||||
|
||||
public enum UniversalVideoGalleryItemContentInfo {
|
||||
case message(Message, Int?)
|
||||
|
|
@ -507,8 +510,19 @@ final class MoreHeaderButton: HighlightableButtonNode {
|
|||
final class SettingsHeaderButton: HighlightableButtonNode {
|
||||
let referenceNode: ContextReferenceContentNode
|
||||
let containerNode: ContextControllerSourceNode
|
||||
private let iconNode: ASImageNode
|
||||
private let iconDotNode: ASImageNode
|
||||
|
||||
private let iconLayer: RasterizedCompositionMonochromeLayer
|
||||
|
||||
private let gearsLayer: RasterizedCompositionImageLayer
|
||||
private let dotLayer: RasterizedCompositionImageLayer
|
||||
|
||||
private var speedBadge: ComponentView<Empty>?
|
||||
private var qualityBadge: ComponentView<Empty>?
|
||||
|
||||
private var speedBadgeText: String?
|
||||
private var qualityBadgeText: String?
|
||||
|
||||
private let badgeFont: UIFont
|
||||
|
||||
private var isMenuOpen: Bool = false
|
||||
|
||||
|
|
@ -523,23 +537,24 @@ final class SettingsHeaderButton: HighlightableButtonNode {
|
|||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.animateScale = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.contentMode = .scaleToFill
|
||||
self.iconLayer = RasterizedCompositionMonochromeLayer()
|
||||
//self.iconLayer.backgroundColor = UIColor.green.cgColor
|
||||
|
||||
self.iconDotNode = ASImageNode()
|
||||
self.iconDotNode.displaysAsynchronously = false
|
||||
self.iconDotNode.displayWithoutProcessing = true
|
||||
self.gearsLayer = RasterizedCompositionImageLayer()
|
||||
self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white)
|
||||
|
||||
self.dotLayer = RasterizedCompositionImageLayer()
|
||||
self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white)
|
||||
|
||||
self.iconLayer.contentsLayer.addSublayer(self.gearsLayer)
|
||||
self.iconLayer.contentsLayer.addSublayer(self.dotLayer)
|
||||
|
||||
self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold)
|
||||
|
||||
super.init()
|
||||
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white)
|
||||
self.iconDotNode.image = generateFilledCircleImage(diameter: 4.0, color: .white)
|
||||
|
||||
self.containerNode.addSubnode(self.referenceNode)
|
||||
self.referenceNode.addSubnode(self.iconNode)
|
||||
self.referenceNode.addSubnode(self.iconDotNode)
|
||||
self.referenceNode.layer.addSublayer(self.iconLayer)
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
|
|
@ -560,17 +575,32 @@ final class SettingsHeaderButton: HighlightableButtonNode {
|
|||
|
||||
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
|
||||
|
||||
if let image = self.iconNode.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
|
||||
self.iconNode.position = iconFrame.center
|
||||
self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||
if let image = self.gearsLayer.image {
|
||||
let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0)
|
||||
let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - iconSize.width) / 2.0), y: floor((self.containerNode.bounds.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
self.iconLayer.position = iconFrame.center
|
||||
self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||
|
||||
if let dotImage = self.iconDotNode.image {
|
||||
let dotFrame = CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - dotImage.size.width) * 0.5), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size)
|
||||
self.iconDotNode.position = dotFrame.center
|
||||
self.iconDotNode.bounds = CGRect(origin: CGPoint(), size: dotFrame.size)
|
||||
self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
|
||||
self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||
|
||||
self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
|
||||
self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||
self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor
|
||||
|
||||
let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size)
|
||||
self.gearsLayer.position = gearsFrame.center
|
||||
self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size)
|
||||
|
||||
if let dotImage = self.dotLayer.image {
|
||||
let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size)
|
||||
self.dotLayer.position = dotFrame.center
|
||||
self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size)
|
||||
}
|
||||
}
|
||||
|
||||
//self.setBadges(speed: "1.5x", quality: "HD", transition: .immediate)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
|
|
@ -592,21 +622,111 @@ final class SettingsHeaderButton: HighlightableButtonNode {
|
|||
self.isMenuOpen = isMenuOpen
|
||||
|
||||
let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
||||
rotationTransition.updateTransform(node: self.iconNode, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0))
|
||||
self.iconNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
||||
rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0))
|
||||
self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
||||
guard let self, finished else {
|
||||
return
|
||||
}
|
||||
self.iconNode.layer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: false)
|
||||
self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true)
|
||||
})
|
||||
|
||||
self.iconDotNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
||||
self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
||||
guard let self, finished else {
|
||||
return
|
||||
}
|
||||
self.iconDotNode.layer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: false)
|
||||
self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true)
|
||||
})
|
||||
}
|
||||
|
||||
func setBadges(speed: String?, quality: String?, transition: ComponentTransition) {
|
||||
if self.speedBadgeText == speed && self.qualityBadgeText == quality {
|
||||
return
|
||||
}
|
||||
self.speedBadgeText = speed
|
||||
self.qualityBadgeText = quality
|
||||
|
||||
if let badgeText = speed {
|
||||
var badgeTransition = transition
|
||||
let speedBadge: ComponentView<Empty>
|
||||
if let current = self.speedBadge {
|
||||
speedBadge = current
|
||||
} else {
|
||||
speedBadge = ComponentView()
|
||||
self.speedBadge = speedBadge
|
||||
badgeTransition = badgeTransition.withAnimation(.none)
|
||||
}
|
||||
let badgeSize = speedBadge.update(
|
||||
transition: badgeTransition,
|
||||
component: AnyComponent(BadgeComponent(
|
||||
text: badgeText,
|
||||
font: self.badgeFont,
|
||||
cornerRadius: 3.0,
|
||||
insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66),
|
||||
outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let speedBadgeView = speedBadge.view {
|
||||
if speedBadgeView.layer.superlayer == nil {
|
||||
self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer)
|
||||
|
||||
transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0)
|
||||
transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0)
|
||||
}
|
||||
badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize))
|
||||
}
|
||||
} else if let speedBadge = self.speedBadge {
|
||||
self.speedBadge = nil
|
||||
if let speedBadgeView = speedBadge.view {
|
||||
transition.setAlpha(layer: speedBadgeView.layer, alpha: 0.0, completion: { [weak speedBadgeView] _ in
|
||||
speedBadgeView?.layer.removeFromSuperlayer()
|
||||
})
|
||||
transition.setScale(layer: speedBadgeView.layer, scale: 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
if let badgeText = quality {
|
||||
var badgeTransition = transition
|
||||
let qualityBadge: ComponentView<Empty>
|
||||
if let current = self.qualityBadge {
|
||||
qualityBadge = current
|
||||
} else {
|
||||
qualityBadge = ComponentView()
|
||||
self.qualityBadge = qualityBadge
|
||||
badgeTransition = badgeTransition.withAnimation(.none)
|
||||
}
|
||||
let badgeSize = qualityBadge.update(
|
||||
transition: badgeTransition,
|
||||
component: AnyComponent(BadgeComponent(
|
||||
text: badgeText,
|
||||
font: self.badgeFont,
|
||||
cornerRadius: 3.0,
|
||||
insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66),
|
||||
outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let qualityBadgeView = qualityBadge.view {
|
||||
if qualityBadgeView.layer.superlayer == nil {
|
||||
self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer)
|
||||
|
||||
transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0)
|
||||
transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0)
|
||||
}
|
||||
badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize))
|
||||
}
|
||||
} else if let qualityBadge = self.qualityBadge {
|
||||
self.qualityBadge = nil
|
||||
if let qualityBadgeView = qualityBadge.view {
|
||||
transition.setAlpha(layer: qualityBadgeView.layer, alpha: 0.0, completion: { [weak qualityBadgeView] _ in
|
||||
qualityBadgeView?.layer.removeFromSuperlayer()
|
||||
})
|
||||
transition.setScale(layer: qualityBadgeView.layer, scale: 0.001)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
|
|
@ -1008,6 +1128,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict
|
|||
private let node: UniversalVideoNode
|
||||
private let willBegin: (NativePictureInPictureContentImpl) -> Void
|
||||
private let didBegin: (NativePictureInPictureContentImpl) -> Void
|
||||
private let didEnd: (NativePictureInPictureContentImpl) -> Void
|
||||
private let expand: (@escaping () -> Void) -> Void
|
||||
private var pictureInPictureTimer: SwiftSignalKit.Timer?
|
||||
private var didExpand: Bool = false
|
||||
|
|
@ -1018,7 +1139,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict
|
|||
|
||||
private var isNativePictureInPictureActiveDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
|
||||
init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didEnd: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
|
||||
self.context = context
|
||||
self.mediaManager = mediaManager
|
||||
self.accountId = accountId
|
||||
|
|
@ -1026,6 +1147,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict
|
|||
self.node = videoNode
|
||||
self.willBegin = willBegin
|
||||
self.didBegin = didBegin
|
||||
self.didEnd = didEnd
|
||||
self.expand = expand
|
||||
|
||||
super.init()
|
||||
|
|
@ -1135,6 +1257,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict
|
|||
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
|
||||
self.hiddenMediaManagerIndex = nil
|
||||
}
|
||||
self.didEnd(self)
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
|
|
@ -1201,6 +1324,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
private var playbackRate: Double?
|
||||
private var videoQuality: UniversalVideoContentVideoQuality = .auto
|
||||
private let playbackRatePromise = ValuePromise<Double>()
|
||||
private let videoQualityPromise = ValuePromise<UniversalVideoContentVideoQuality>()
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
private let moreButtonStateDisposable = MetaDisposable()
|
||||
|
|
@ -1562,8 +1686,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
isAdaptive = true
|
||||
}
|
||||
|
||||
//TODO:release
|
||||
//self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
|
||||
let _ = isAdaptive
|
||||
|
||||
let dimensions = item.content.dimensions
|
||||
|
|
@ -1705,46 +1827,38 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
/*self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
|
||||
self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
|
||||
self.playbackRatePromise.get(),
|
||||
self.isShowingContextMenuPromise.get()
|
||||
).start(next: { [weak self] playbackRate, isShowingContextMenu in
|
||||
guard let strongSelf = self else {
|
||||
self.videoQualityPromise.get()
|
||||
).start(next: { [weak self] playbackRate, videoQuality in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let effectiveBaseRate: Double
|
||||
if isShowingContextMenu {
|
||||
effectiveBaseRate = 1.0
|
||||
} else {
|
||||
effectiveBaseRate = playbackRate
|
||||
|
||||
var rateString: String?
|
||||
if abs(playbackRate - 1.0) > 0.1 {
|
||||
var stringValue = String(format: "%.1fx", playbackRate)
|
||||
if stringValue.hasSuffix(".0x") {
|
||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||
}
|
||||
rateString = stringValue
|
||||
}
|
||||
|
||||
var qualityString: String?
|
||||
if case let .quality(quality) = videoQuality {
|
||||
if quality <= 360 {
|
||||
qualityString = "LD"
|
||||
} else if quality <= 480 {
|
||||
qualityString = "SD"
|
||||
} else if quality <= 720 {
|
||||
qualityString = "HD"
|
||||
} else {
|
||||
qualityString = "UHD"
|
||||
}
|
||||
}
|
||||
|
||||
if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 {
|
||||
strongSelf.moreBarButtonRate = effectiveBaseRate
|
||||
let animated: Bool
|
||||
if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp {
|
||||
animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2)
|
||||
} else {
|
||||
animated = false
|
||||
}
|
||||
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
if abs(effectiveBaseRate - 1.0) > 0.01 {
|
||||
var stringValue = String(format: "%.1fx", effectiveBaseRate)
|
||||
if stringValue.hasSuffix(".0x") {
|
||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||
}
|
||||
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated)
|
||||
} else {
|
||||
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
|
||||
}
|
||||
} else {
|
||||
if strongSelf.moreBarButtonRateTimestamp == nil {
|
||||
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
}
|
||||
}))*/
|
||||
self.settingsBarButton.setBadges(speed: rateString, quality: qualityString, transition: .spring(duration: 0.35))
|
||||
}))
|
||||
|
||||
self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in
|
||||
|
|
@ -1996,6 +2110,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
||||
self.videoQualityPromise.set(self.videoQuality)
|
||||
|
||||
var isAd = false
|
||||
if let contentInfo = item.contentInfo {
|
||||
|
|
@ -2587,6 +2702,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
})
|
||||
}
|
||||
|
||||
override func maybePerformActionForSwipeDismiss() -> Bool {
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_swipe_pip"] {
|
||||
return false
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
|
||||
nativePictureInPictureContent.beginPictureInPicture()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func title() -> Signal<String, NoError> {
|
||||
return self._title.get()
|
||||
}
|
||||
|
|
@ -2760,6 +2889,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
var didExpand = false
|
||||
let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in
|
||||
guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else {
|
||||
return
|
||||
|
|
@ -2771,12 +2901,30 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.completeCustomDismiss(true)
|
||||
})
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.setNativePictureInPictureIsActive(false)
|
||||
}
|
||||
didExpand = false
|
||||
}, didBegin: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, didEnd: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.setNativePictureInPictureIsActive(false)
|
||||
}
|
||||
|
||||
if !didExpand {
|
||||
self.activePictureInPictureController = nil
|
||||
self.activePictureInPictureNavigationController = nil
|
||||
}
|
||||
}, expand: { [weak self] completion in
|
||||
didExpand = true
|
||||
|
||||
guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else {
|
||||
completion()
|
||||
return
|
||||
|
|
@ -2790,9 +2938,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
})
|
||||
|
||||
activePictureInPictureController.view.alpha = 1.0
|
||||
activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
|
||||
completion()
|
||||
activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35, completion: { _ in
|
||||
})
|
||||
|
||||
completion()
|
||||
})
|
||||
|
||||
self.nativePictureInPictureContent = content
|
||||
|
|
@ -3333,8 +3482,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return
|
||||
}
|
||||
videoNode.setVideoQuality(.auto)
|
||||
//TODO:release
|
||||
//self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
|
||||
self.videoQualityPromise.set(.auto)
|
||||
|
||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||
controller.updateSharedPlaybackRate(rate)
|
||||
|
|
@ -3367,12 +3515,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return
|
||||
}
|
||||
videoNode.setVideoQuality(.quality(quality))
|
||||
//TODO:release
|
||||
/*if quality >= 700 {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white)))
|
||||
} else {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white)))
|
||||
}*/
|
||||
self.videoQualityPromise.set(.quality(quality))
|
||||
|
||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||
controller.updateSharedPlaybackRate(rate)
|
||||
|
|
@ -3407,15 +3550,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
c?.popItems()
|
||||
})))
|
||||
|
||||
for quality in qualityState.available {
|
||||
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
||||
continue
|
||||
}
|
||||
let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in
|
||||
guard let qualityFileSize = qualityFile.media.size else {
|
||||
continue
|
||||
return
|
||||
}
|
||||
let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
|
||||
items.append(.action(ContextMenuActionItem(text: "Save in \(quality)p", textLayout: .secondLineWithValue(fileSizeString), icon: { _ in
|
||||
let title: String
|
||||
if let quality {
|
||||
title = "Save in \(quality)p"
|
||||
} else {
|
||||
title = "Save Original"
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: .secondLineWithValue(fileSizeString), icon: { _ in
|
||||
return nil
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
|
|
@ -3458,6 +3604,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
})))
|
||||
}
|
||||
|
||||
if self.context.isPremium {
|
||||
addItem(nil, content.fileReference)
|
||||
} else {
|
||||
#if DEBUG
|
||||
addItem(nil, content.fileReference)
|
||||
#endif
|
||||
}
|
||||
|
||||
for quality in qualityState.available {
|
||||
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
||||
continue
|
||||
}
|
||||
addItem(quality, qualityFile)
|
||||
}
|
||||
|
||||
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
|
||||
} else {
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
|
|
@ -3683,6 +3844,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||
self.videoQuality = videoQuality
|
||||
self.videoQualityPromise.set(videoQuality)
|
||||
|
||||
self.videoNode?.setVideoQuality(videoQuality)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,14 +448,22 @@ private func chatMessageImageFileThumbnailDatas(account: Account, userLocation:
|
|||
return signal
|
||||
}
|
||||
|
||||
private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError> {
|
||||
private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError> {
|
||||
let fullSizeResource = fileReference.media.resource
|
||||
var reducedSizeResource: MediaResource?
|
||||
if let videoThumbnail = fileReference.media.videoThumbnails.first {
|
||||
if let previewSourceFileReference, let videoThumbnail = previewSourceFileReference.media.videoThumbnails.first {
|
||||
reducedSizeResource = videoThumbnail.resource
|
||||
} else if let videoThumbnail = fileReference.media.videoThumbnails.first {
|
||||
reducedSizeResource = videoThumbnail.resource
|
||||
}
|
||||
|
||||
let thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations)
|
||||
var thumbnailRepresentation: TelegramMediaImageRepresentation?
|
||||
if let previewSourceFileReference {
|
||||
thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(previewSourceFileReference.media.previewRepresentations) : smallestImageRepresentation(previewSourceFileReference.media.previewRepresentations)
|
||||
}
|
||||
if thumbnailRepresentation == nil {
|
||||
thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations)
|
||||
}
|
||||
let thumbnailResource = thumbnailRepresentation?.resource
|
||||
|
||||
let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad)
|
||||
|
|
@ -979,7 +987,7 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou
|
|||
}
|
||||
|
||||
public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false, synchronousLoads: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred)
|
||||
let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, previewSourceFileReference: nil, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred)
|
||||
|
||||
return signal
|
||||
|> map { value in
|
||||
|
|
@ -1581,7 +1589,7 @@ public func mediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceU
|
|||
}
|
||||
}
|
||||
|
||||
public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
|
||||
public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
|
||||
let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError>
|
||||
if let imageReference = imageReference {
|
||||
signal = chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad, forceThumbnail: blurred)
|
||||
|
|
@ -1592,7 +1600,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaR
|
|||
return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete)
|
||||
}
|
||||
} else {
|
||||
signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred)
|
||||
signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, previewSourceFileReference: previewSourceFileReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred)
|
||||
}
|
||||
|
||||
return signal
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ private var declaredEncodables: Void = {
|
|||
declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) })
|
||||
declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) })
|
||||
declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) })
|
||||
declareEncodable(WasScheduledMessageAttribute.self, f: { WasScheduledMessageAttribute(decoder: $0) })
|
||||
declareEncodable(PendingProcessingMessageAttribute.self, f: { PendingProcessingMessageAttribute(decoder: $0) })
|
||||
declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
|
||||
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
|
||||
declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) })
|
||||
|
|
|
|||
|
|
@ -655,6 +655,12 @@ extension StoreMessage {
|
|||
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||
switch apiMessage {
|
||||
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck):
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
if (flags2 & (1 << 4)) != 0 {
|
||||
attributes.append(PendingProcessingMessageAttribute(approximateCompletionTime: date))
|
||||
}
|
||||
|
||||
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
|
||||
|
||||
var namespace = namespace
|
||||
|
|
@ -676,8 +682,6 @@ extension StoreMessage {
|
|||
authorId = resolvedFromId
|
||||
}
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
var threadId: Int64?
|
||||
if let replyTo = replyTo {
|
||||
var threadMessageId: MessageId?
|
||||
|
|
|
|||
|
|
@ -258,7 +258,8 @@ private final class PendingPeerMediaUploadManagerImpl {
|
|||
message: message,
|
||||
cacheReferenceKey: nil,
|
||||
result: result,
|
||||
accountPeerId: accountPeerId
|
||||
accountPeerId: accountPeerId,
|
||||
pendingMessageEvent: { _ in }
|
||||
)
|
||||
|> deliverOn(queue)).start(completed: { [weak self, weak context] in
|
||||
guard let strongSelf = self, let initialContext = context else {
|
||||
|
|
|
|||
|
|
@ -3286,6 +3286,8 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
|
|||
result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages))
|
||||
}
|
||||
currentAddMessages = nil
|
||||
currentAddScheduledMessages = nil
|
||||
currentAddQuickReplyMessages = nil
|
||||
result.append(operation)
|
||||
case let .UpdateState(state):
|
||||
updatedState = state
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force:
|
|||
}
|
||||
}
|
||||
|
||||
func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId) -> Signal<Void, NoError> {
|
||||
func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId, pendingMessageEvent: @escaping (PeerPendingMessageDelivered) -> Void) -> Signal<Void, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
let messageId: Int32?
|
||||
var apiMessage: Api.Message?
|
||||
|
|
@ -125,7 +125,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||
var sentStickers: [TelegramMediaFile] = []
|
||||
var sentGifs: [TelegramMediaFile] = []
|
||||
|
||||
if let updatedTimestamp = updatedTimestamp {
|
||||
if let updatedTimestamp {
|
||||
transaction.offsetPendingMessagesTimestamps(lowerBound: message.id, excludeIds: Set([message.id]), timestamp: updatedTimestamp)
|
||||
}
|
||||
|
||||
|
|
@ -134,23 +134,6 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
|
||||
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
let updatedId: MessageId
|
||||
if let messageId = messageId {
|
||||
var namespace: MessageId.Namespace = Namespaces.Message.Cloud
|
||||
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let updatedTimestamp = updatedTimestamp {
|
||||
if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
} else if Namespaces.Message.allScheduled.contains(message.id.namespace) {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId)
|
||||
} else {
|
||||
updatedId = currentMessage.id
|
||||
}
|
||||
|
||||
let media: [Media]
|
||||
var attributes: [MessageAttribute]
|
||||
let text: String
|
||||
|
|
@ -195,12 +178,10 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||
updatedAttributes.append(MediaSpoilerMessageAttribute())
|
||||
}
|
||||
|
||||
if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud {
|
||||
for i in 0 ..< updatedAttributes.count {
|
||||
if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute {
|
||||
updatedAttributes.remove(at: i)
|
||||
break
|
||||
}
|
||||
for i in 0 ..< updatedAttributes.count {
|
||||
if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute {
|
||||
updatedAttributes.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
|
||||
|
|
@ -225,6 +206,30 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||
threadId = currentMessage.threadId
|
||||
}
|
||||
|
||||
let updatedId: MessageId
|
||||
if let messageId = messageId {
|
||||
var namespace: MessageId.Namespace = Namespaces.Message.Cloud
|
||||
if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let updatedTimestamp = updatedTimestamp {
|
||||
if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
} else {
|
||||
if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
}
|
||||
} else if Namespaces.Message.allScheduled.contains(message.id.namespace) {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId)
|
||||
} else {
|
||||
updatedId = currentMessage.id
|
||||
}
|
||||
|
||||
for attribute in currentMessage.attributes {
|
||||
if let attribute = attribute as? OutgoingMessageInfoAttribute {
|
||||
bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets
|
||||
|
|
@ -358,12 +363,26 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||
|
||||
stateManager.addUpdates(result)
|
||||
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: message.id.peerId)])
|
||||
|
||||
if let updatedMessage, case let .Id(id) = updatedMessage.id {
|
||||
pendingMessageEvent(PeerPendingMessageDelivered(
|
||||
id: id,
|
||||
isSilent: updatedMessage.attributes.contains(where: { attribute in
|
||||
if let attribute = attribute as? NotificationInfoMessageAttribute {
|
||||
return attribute.flags.contains(.muted)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute })
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal<Void, NoError> {
|
||||
func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates, pendingMessageEvents: @escaping ([PeerPendingMessageDelivered]) -> Void) -> Signal<Void, NoError> {
|
||||
guard !messages.isEmpty else {
|
||||
return .complete()
|
||||
return .single(Void())
|
||||
}
|
||||
|
||||
return postbox.transaction { transaction -> Void in
|
||||
|
|
@ -372,8 +391,12 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
|||
var namespace = Namespaces.Message.Cloud
|
||||
if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
} else if let message = messages.first, let apiMessage = result.messages.first {
|
||||
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
}
|
||||
|
||||
var resultMessages: [MessageId: StoreMessage] = [:]
|
||||
|
|
@ -538,6 +561,23 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
|||
}
|
||||
stateManager.addUpdates(result)
|
||||
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: messages[0].id.peerId)])
|
||||
|
||||
pendingMessageEvents(mapping.compactMap { message, _, updatedMessage -> PeerPendingMessageDelivered? in
|
||||
guard case let .Id(id) = updatedMessage.id else {
|
||||
return nil
|
||||
}
|
||||
return PeerPendingMessageDelivered(
|
||||
id: id,
|
||||
isSilent: updatedMessage.attributes.contains(where: { attribute in
|
||||
if let attribute = attribute as? NotificationInfoMessageAttribute {
|
||||
return attribute.flags.contains(.muted)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute })
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,20 @@ func sendMessageReasonForError(_ error: String) -> PendingMessageFailureReason?
|
|||
}
|
||||
}
|
||||
|
||||
public struct PeerPendingMessageDelivered {
|
||||
public var id: EngineMessage.Id
|
||||
public var isSilent: Bool
|
||||
public var isPendingProcessing: Bool
|
||||
|
||||
public init(id: EngineMessage.Id, isSilent: Bool, isPendingProcessing: Bool) {
|
||||
self.id = id
|
||||
self.isSilent = isSilent
|
||||
self.isPendingProcessing = isPendingProcessing
|
||||
}
|
||||
}
|
||||
|
||||
private final class PeerPendingMessagesSummaryContext {
|
||||
var messageDeliveredSubscribers = Bag<((MessageId.Namespace, Bool)) -> Void>()
|
||||
var messageDeliveredSubscribers = Bag<([PeerPendingMessageDelivered]) -> Void>()
|
||||
var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>()
|
||||
}
|
||||
|
||||
|
|
@ -270,29 +282,32 @@ public final class PendingMessageManager {
|
|||
}
|
||||
|
||||
if !removedSecretMessageIds.isEmpty {
|
||||
let _ = (self.postbox.transaction { transaction -> (Set<PeerId>, Bool) in
|
||||
var silent = false
|
||||
var peerIdsWithDeliveredMessages = Set<PeerId>()
|
||||
let _ = (self.postbox.transaction { transaction -> [PeerId: [PeerPendingMessageDelivered]] in
|
||||
var peerIdsWithDeliveredMessages: [PeerId: [PeerPendingMessageDelivered]] = [:]
|
||||
for id in removedSecretMessageIds {
|
||||
if let message = transaction.getMessage(id) {
|
||||
if message.isSentOrAcknowledged {
|
||||
peerIdsWithDeliveredMessages.insert(id.peerId)
|
||||
var silent = false
|
||||
if message.muted {
|
||||
silent = true
|
||||
}
|
||||
if peerIdsWithDeliveredMessages[id.peerId] == nil {
|
||||
peerIdsWithDeliveredMessages[id.peerId] = []
|
||||
}
|
||||
peerIdsWithDeliveredMessages[id.peerId]?.append(PeerPendingMessageDelivered(id: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: id.id), isSilent: silent, isPendingProcessing: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
return (peerIdsWithDeliveredMessages, silent)
|
||||
return peerIdsWithDeliveredMessages
|
||||
}
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages, silent in
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
for peerId in peerIdsWithDeliveredMessages {
|
||||
for (peerId, deliveredMessages) in peerIdsWithDeliveredMessages {
|
||||
if let context = strongSelf.peerSummaryContexts[peerId] {
|
||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
||||
subscriber((Namespaces.Message.Cloud, silent))
|
||||
subscriber(deliveredMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1724,43 +1739,49 @@ public final class PendingMessageManager {
|
|||
}
|
||||
}
|
||||
|
||||
let silent = message.muted
|
||||
var namespace = Namespaces.Message.Cloud
|
||||
if message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) {
|
||||
namespace = id.namespace
|
||||
|
||||
if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId {
|
||||
self.correlationIdToSentMessageId.with { value in
|
||||
value.mapping[correlationId] = id
|
||||
} else if let apiMessage {
|
||||
var isScheduled = false
|
||||
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||
isScheduled = true
|
||||
}
|
||||
if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage {
|
||||
if (flags2 & (1 << 4)) != 0 {
|
||||
isScheduled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId)
|
||||
|> afterDisposed { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.queue.async {
|
||||
if let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
||||
subscriber((namespace, silent))
|
||||
}
|
||||
if let id = apiMessage.id(namespace: isScheduled ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) {
|
||||
if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId {
|
||||
self.correlationIdToSentMessageId.with { value in
|
||||
value.mapping[correlationId] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let queue = self.queue
|
||||
return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId, pendingMessageEvent: { [weak self] pendingMessageDelivered in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
if let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
||||
subscriber([pendingMessageDelivered])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal<Void, NoError> {
|
||||
var silent = false
|
||||
var namespace = Namespaces.Message.Cloud
|
||||
if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
if message.muted {
|
||||
silent = true
|
||||
if let message = messages.first {
|
||||
if message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
||||
namespace = Namespaces.Message.QuickReplyCloud
|
||||
} else if let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1777,22 +1798,22 @@ public final class PendingMessageManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
let queue = self.queue
|
||||
|
||||
return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result)
|
||||
|> afterDisposed { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.queue.async {
|
||||
if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
||||
return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result, pendingMessageEvents: { [weak self] pendingMessagesDelivered in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId], !pendingMessagesDelivered.isEmpty {
|
||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
||||
subscriber((namespace, silent))
|
||||
subscriber(pendingMessagesDelivered)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func deliveredMessageEvents(peerId: PeerId) -> Signal<(namespace: MessageId.Namespace, silent: Bool), NoError> {
|
||||
public func deliveredMessageEvents(peerId: PeerId) -> Signal<[PeerPendingMessageDelivered], NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
|
|
@ -1805,8 +1826,8 @@ public final class PendingMessageManager {
|
|||
self.peerSummaryContexts[peerId] = summaryContext
|
||||
}
|
||||
|
||||
let index = summaryContext.messageDeliveredSubscribers.add({ namespace, silent in
|
||||
subscriber.putNext((namespace, silent))
|
||||
let index = summaryContext.messageDeliveredSubscribers.add({ event in
|
||||
subscriber.putNext(event)
|
||||
})
|
||||
|
||||
disposable.set(ActionDisposable {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,11 @@ extension Api.Message {
|
|||
|
||||
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
|
||||
switch self {
|
||||
case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
var namespace = namespace
|
||||
if (flags2 & (1 << 4)) != 0 {
|
||||
namespace = Namespaces.Message.ScheduledCloud
|
||||
}
|
||||
let peerId: PeerId = messagePeerId.peerId
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: id)
|
||||
case let .messageEmpty(_, id, peerId):
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
|
||||
public class WasScheduledMessageAttribute: MessageAttribute {
|
||||
public init() {
|
||||
public class PendingProcessingMessageAttribute: MessageAttribute {
|
||||
public let approximateCompletionTime: Int32
|
||||
|
||||
public init(approximateCompletionTime: Int32) {
|
||||
self.approximateCompletionTime = approximateCompletionTime
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.approximateCompletionTime = decoder.decodeInt32ForKey("et", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.approximateCompletionTime, forKey: "et")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,6 +579,17 @@ public extension Message {
|
|||
}
|
||||
}
|
||||
|
||||
public extension Message {
|
||||
var pendingProcessingAttribute: PendingProcessingMessageAttribute? {
|
||||
for attribute in self.attributes {
|
||||
if let attribute = attribute as? PendingProcessingMessageAttribute {
|
||||
return attribute
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension Message {
|
||||
func areReactionsTags(accountPeerId: PeerId) -> Bool {
|
||||
if self.id.peerId == accountPeerId {
|
||||
|
|
|
|||
20
submodules/TelegramUI/Components/BadgeComponent/BUILD
Normal file
20
submodules/TelegramUI/Components/BadgeComponent/BUILD
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "BadgeComponent",
|
||||
module_name = "BadgeComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/RasterizedCompositionComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import RasterizedCompositionComponent
|
||||
import ComponentFlow
|
||||
|
||||
public final class BadgeComponent: Component {
|
||||
public let text: String
|
||||
public let font: UIFont
|
||||
public let cornerRadius: CGFloat
|
||||
public let insets: UIEdgeInsets
|
||||
public let outerInsets: UIEdgeInsets
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
font: UIFont,
|
||||
cornerRadius: CGFloat,
|
||||
insets: UIEdgeInsets,
|
||||
outerInsets: UIEdgeInsets
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.cornerRadius = cornerRadius
|
||||
self.insets = insets
|
||||
self.outerInsets = outerInsets
|
||||
}
|
||||
|
||||
public static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if lhs.outerInsets != rhs.outerInsets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct TextLayout {
|
||||
var size: CGSize
|
||||
var opticalBounds: CGRect
|
||||
|
||||
init(size: CGSize, opticalBounds: CGRect) {
|
||||
self.size = size
|
||||
self.opticalBounds = opticalBounds
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
override public static var layerClass: AnyClass {
|
||||
return RasterizedCompositionLayer.self
|
||||
}
|
||||
|
||||
private let contentsClippingLayer: RasterizedCompositionLayer
|
||||
private let backgroundInsetLayer: RasterizedCompositionImageLayer
|
||||
private let backgroundLayer: RasterizedCompositionImageLayer
|
||||
private let textContentsLayer: RasterizedCompositionImageLayer
|
||||
|
||||
private var textLayout: TextLayout?
|
||||
|
||||
private var component: BadgeComponent?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.contentsClippingLayer = RasterizedCompositionLayer()
|
||||
self.backgroundInsetLayer = RasterizedCompositionImageLayer()
|
||||
self.backgroundLayer = RasterizedCompositionImageLayer()
|
||||
|
||||
self.textContentsLayer = RasterizedCompositionImageLayer()
|
||||
self.textContentsLayer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.backgroundInsetLayer)
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
self.layer.addSublayer(self.contentsClippingLayer)
|
||||
self.contentsClippingLayer.addSublayer(self.textContentsLayer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
if component.text != previousComponent?.text || component.font != previousComponent?.font {
|
||||
let attributedText = NSAttributedString(string: component.text, attributes: [
|
||||
NSAttributedString.Key.font: component.font,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.black
|
||||
])
|
||||
|
||||
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
|
||||
context.withContext { c in
|
||||
UIGraphicsPushContext(c)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
attributedText.draw(at: CGPoint())
|
||||
}
|
||||
var minFilledLineY = Int(context.scaledSize.height) - 1
|
||||
var maxFilledLineY = 0
|
||||
var minFilledLineX = Int(context.scaledSize.width) - 1
|
||||
var maxFilledLineX = 0
|
||||
for y in 0 ..< Int(context.scaledSize.height) {
|
||||
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
for x in 0 ..< Int(context.scaledSize.width) {
|
||||
let pixelPtr = linePtr.advanced(by: x)
|
||||
if pixelPtr.pointee != 0 {
|
||||
minFilledLineY = min(y, minFilledLineY)
|
||||
maxFilledLineY = max(y, maxFilledLineY)
|
||||
minFilledLineX = min(x, minFilledLineX)
|
||||
maxFilledLineX = max(x, maxFilledLineX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var opticalBounds = CGRect()
|
||||
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
|
||||
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
|
||||
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
|
||||
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
|
||||
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
|
||||
}
|
||||
|
||||
self.textContentsLayer.image = context.generateImage()
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
|
||||
} else {
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
|
||||
}
|
||||
}
|
||||
|
||||
if component.cornerRadius != previousComponent?.cornerRadius {
|
||||
self.backgroundLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .white)
|
||||
|
||||
self.backgroundInsetLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .black)
|
||||
}
|
||||
|
||||
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
|
||||
|
||||
let size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
|
||||
transition.setFrame(layer: self.contentsClippingLayer, frame: backgroundFrame)
|
||||
|
||||
let outerInsetsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX - component.outerInsets.left, y: backgroundFrame.minY - component.outerInsets.top), size: CGSize(width: backgroundFrame.width + component.outerInsets.left + component.outerInsets.right, height: backgroundFrame.height + component.outerInsets.top + component.outerInsets.bottom))
|
||||
transition.setFrame(layer: self.backgroundInsetLayer, frame: outerInsetsFrame)
|
||||
|
||||
var textFrame = CGRect(origin: CGPoint(x: component.insets.left, y: component.insets.top), size: textSize)
|
||||
if let textLayout = self.textLayout {
|
||||
textFrame.origin.x = -textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
|
||||
textFrame.origin.y = -textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
|
||||
}
|
||||
|
||||
transition.setPosition(layer: self.textContentsLayer, position: textFrame.origin)
|
||||
self.textContentsLayer.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -95,17 +95,9 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
|
|||
dateText = " "
|
||||
}
|
||||
|
||||
/*if "".isEmpty, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile, file.isVideo, !file.isInstantVideo, !file.isAnimated {
|
||||
if message.id.namespace == Namespaces.Message.ScheduledCloud {
|
||||
return "appx. \(dateText)"
|
||||
} else if message.id.namespace == Namespaces.Message.ScheduledLocal {
|
||||
return "processing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute {
|
||||
return "appx. \(dateText)"
|
||||
}
|
||||
|
||||
if displayFullDate {
|
||||
let dayText: String
|
||||
|
|
|
|||
|
|
@ -1536,6 +1536,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
}
|
||||
if !reloadMedia {
|
||||
updateImageSignal = nil
|
||||
} else {
|
||||
print("reload media")
|
||||
}
|
||||
|
||||
var isExtendedMedia = false
|
||||
|
|
@ -1719,6 +1721,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
enableSound: false,
|
||||
fetchAutomatically: false,
|
||||
onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false),
|
||||
autoFetchFullSizeThumbnail: true,
|
||||
continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo,
|
||||
placeholderColor: emptyColor,
|
||||
captureProtected: message.isCopyProtected() || isExtendedMedia,
|
||||
|
|
@ -1747,6 +1750,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
videoNode.isHidden = true
|
||||
strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
|
||||
}
|
||||
//videoNode.alpha = 0.5
|
||||
|
||||
updatedVideoNodeReadySignal = videoNode.ready
|
||||
updatedPlayerStatusSignal = videoNode.status
|
||||
|
|
@ -1990,6 +1994,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
prefixSeconds: 10,
|
||||
autofetchPlaylist: false
|
||||
)
|
||||
//|> delay(2.0, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak strongSelf] preloadData in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String
|
|||
}
|
||||
|
||||
private func dateHeaderTimestampId(timestamp: Int32) -> Int32 {
|
||||
if timestamp == scheduleWhenOnlineTimestamp {
|
||||
if timestamp == scheduleWhenOnlineTimestamp || timestamp >= Int32.max - 1000 {
|
||||
return timestamp
|
||||
} else if timestamp == Int32.max {
|
||||
return timestamp / (granularity) * (granularity)
|
||||
|
|
|
|||
|
|
@ -566,4 +566,11 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override public func getStatusNode() -> ASDisplayNode? {
|
||||
if !self.interactiveImageNode.dateAndStatusNode.isHidden {
|
||||
return self.interactiveImageNode.dateAndStatusNode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "RasterizedCompositionComponent",
|
||||
module_name = "RasterizedCompositionComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import UIKitRuntimeUtils
|
||||
import ComponentFlow
|
||||
|
||||
open class RasterizedCompositionLayer: CALayer {
|
||||
private final class SublayerReference {
|
||||
weak var layer: CALayer?
|
||||
|
||||
init(layer: CALayer) {
|
||||
self.layer = layer
|
||||
}
|
||||
}
|
||||
|
||||
private var sublayerReferences: [SublayerReference] = []
|
||||
|
||||
public var onUpdatedIsAnimating: (() -> Void)?
|
||||
public var onContentsUpdated: (() -> Void)?
|
||||
|
||||
override public var position: CGPoint {
|
||||
didSet {
|
||||
if self.position != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var bounds: CGRect {
|
||||
didSet {
|
||||
if self.bounds != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var transform: CATransform3D {
|
||||
didSet {
|
||||
if !CATransform3DEqualToTransform(self.transform, oldValue) {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var opacity: Float {
|
||||
didSet {
|
||||
if self.opacity != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var isHidden: Bool {
|
||||
didSet {
|
||||
if self.isHidden != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var backgroundColor: CGColor? {
|
||||
didSet {
|
||||
if let lhs = self.backgroundColor, let rhs = oldValue {
|
||||
if lhs != rhs {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
} else if (self.backgroundColor == nil) != (oldValue == nil) {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var cornerRadius: CGFloat {
|
||||
didSet {
|
||||
if self.cornerRadius != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var masksToBounds: Bool {
|
||||
didSet {
|
||||
if self.masksToBounds != oldValue {
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var hasAnimationsInTree: Bool {
|
||||
if let animationKeys = self.animationKeys(), !animationKeys.isEmpty {
|
||||
return true
|
||||
}
|
||||
if let sublayers = self.sublayers {
|
||||
for sublayer in sublayers {
|
||||
if let sublayer = sublayer as? RasterizedCompositionLayer {
|
||||
if sublayer.hasAnimationsInTree {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func onLayerAdded(layer: CALayer) {
|
||||
if !self.sublayerReferences.contains(where: { $0.layer === layer }) {
|
||||
self.sublayerReferences.append(SublayerReference(layer: layer))
|
||||
}
|
||||
if let layer = layer as? RasterizedCompositionLayer {
|
||||
layer.onUpdatedIsAnimating = { [weak self] in
|
||||
self?.onUpdatedIsAnimating?()
|
||||
}
|
||||
layer.onContentsUpdated = { [weak self] in
|
||||
self?.onContentsUpdated?()
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
self.onUpdatedIsAnimating?()
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
|
||||
private func cleanupSublayerReferences() {
|
||||
for i in (0 ..< self.sublayerReferences.count).reversed() {
|
||||
if let layer = sublayerReferences[i].layer {
|
||||
if layer.superlayer !== self {
|
||||
sublayerReferences.remove(at: i)
|
||||
}
|
||||
} else {
|
||||
sublayerReferences.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func addSublayer(_ layer: CALayer) {
|
||||
super.addSublayer(layer)
|
||||
|
||||
self.onLayerAdded(layer: layer)
|
||||
}
|
||||
|
||||
override public func insertSublayer(_ layer: CALayer, at idx: UInt32) {
|
||||
super.insertSublayer(layer, at: idx)
|
||||
|
||||
self.onLayerAdded(layer: layer)
|
||||
}
|
||||
|
||||
override public func insertSublayer(_ layer: CALayer, below sibling: CALayer?) {
|
||||
super.insertSublayer(layer, below: sibling)
|
||||
|
||||
self.onLayerAdded(layer: layer)
|
||||
}
|
||||
|
||||
override public func insertSublayer(_ layer: CALayer, above sibling: CALayer?) {
|
||||
super.insertSublayer(layer, above: sibling)
|
||||
|
||||
self.onLayerAdded(layer: layer)
|
||||
}
|
||||
|
||||
override public func replaceSublayer(_ oldLayer: CALayer, with newLayer: CALayer) {
|
||||
super.replaceSublayer(oldLayer, with: newLayer)
|
||||
|
||||
self.onLayerAdded(layer: newLayer)
|
||||
}
|
||||
|
||||
override public func add(_ anim: CAAnimation, forKey key: String?) {
|
||||
let anim = anim.copy() as! CAAnimation
|
||||
let completion = anim.completion
|
||||
anim.completion = { [weak self] flag in
|
||||
completion?(flag)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.onUpdatedIsAnimating?()
|
||||
}
|
||||
|
||||
super.add(anim, forKey: key)
|
||||
}
|
||||
|
||||
override public func removeAllAnimations() {
|
||||
super.removeAllAnimations()
|
||||
|
||||
self.onUpdatedIsAnimating?()
|
||||
}
|
||||
|
||||
override public func removeAnimation(forKey key: String) {
|
||||
super.removeAnimation(forKey: key)
|
||||
|
||||
if let animationKeys = self.animationKeys(), !animationKeys.isEmpty {
|
||||
} else {
|
||||
self.onUpdatedIsAnimating?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class RasterizedCompositionImageLayer: RasterizedCompositionLayer {
|
||||
public var image: UIImage? {
|
||||
didSet {
|
||||
if self.image !== oldValue {
|
||||
if let image = self.image {
|
||||
let capInsets = image.capInsets
|
||||
if capInsets.left.isZero && capInsets.top.isZero && capInsets.right.isZero && capInsets.bottom.isZero {
|
||||
self.contentsScale = image.scale
|
||||
self.contents = image.cgImage
|
||||
} else {
|
||||
ASDisplayNodeSetResizableContents(self, image)
|
||||
}
|
||||
} else {
|
||||
self.contents = nil
|
||||
}
|
||||
self.onContentsUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateSublayerBounds(layer: CALayer) -> CGRect {
|
||||
var result: CGRect
|
||||
if layer.contents != nil {
|
||||
result = layer.bounds
|
||||
} else {
|
||||
result = CGRect()
|
||||
}
|
||||
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
let sublayerBounds = sublayer.convert(sublayer.bounds, to: layer)
|
||||
if result.isEmpty {
|
||||
result = sublayerBounds
|
||||
} else {
|
||||
result = result.union(sublayerBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public final class RasterizedCompositionMonochromeLayer: SimpleLayer {
|
||||
public let contentsLayer = RasterizedCompositionLayer()
|
||||
public let maskedLayer = SimpleLayer()
|
||||
public let rasterizedLayer = SimpleLayer()
|
||||
|
||||
private var isContentsUpdateScheduled: Bool = false
|
||||
private var isRasterizationModeUpdateScheduled: Bool = false
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.maskedLayer.isHidden = true
|
||||
self.addSublayer(self.maskedLayer)
|
||||
|
||||
self.maskedLayer.mask = self.contentsLayer
|
||||
self.maskedLayer.rasterizationScale = UIScreenScale
|
||||
|
||||
self.contentsLayer.backgroundColor = UIColor.black.cgColor
|
||||
if let filter = makeLuminanceToAlphaFilter() {
|
||||
self.contentsLayer.filters = [filter]
|
||||
}
|
||||
self.contentsLayer.rasterizationScale = UIScreenScale
|
||||
|
||||
self.addSublayer(self.rasterizedLayer)
|
||||
|
||||
self.contentsLayer.onContentsUpdated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.contentsLayer.hasAnimationsInTree {
|
||||
self.scheduleContentsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
self.contentsLayer.onUpdatedIsAnimating = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.scheduleUpdateRasterizationMode()
|
||||
}
|
||||
|
||||
self.isContentsUpdateScheduled = true
|
||||
self.isRasterizationModeUpdateScheduled = true
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
private func scheduleContentsUpdate() {
|
||||
self.isContentsUpdateScheduled = true
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
private func scheduleUpdateRasterizationMode() {
|
||||
self.isRasterizationModeUpdateScheduled = true
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
override public func layoutSublayers() {
|
||||
super.layoutSublayers()
|
||||
|
||||
if self.isRasterizationModeUpdateScheduled {
|
||||
self.isRasterizationModeUpdateScheduled = false
|
||||
self.updateRasterizationMode()
|
||||
}
|
||||
if self.isContentsUpdateScheduled {
|
||||
self.isContentsUpdateScheduled = false
|
||||
if !self.contentsLayer.hasAnimationsInTree {
|
||||
self.updateContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateContents() {
|
||||
var contentBounds = calculateSublayerBounds(layer: self.contentsLayer)
|
||||
contentBounds.size.width = ceil(contentBounds.width)
|
||||
contentBounds.size.height = ceil(contentBounds.height)
|
||||
self.rasterizedLayer.frame = contentBounds
|
||||
let contentsImage = generateImage(contentBounds.size, rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.translateBy(x: -contentBounds.minX, y: -contentBounds.minY)
|
||||
|
||||
self.contentsLayer.render(in: context)
|
||||
})
|
||||
|
||||
if let contentsImage {
|
||||
if let context = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true), let alphaContext = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true) {
|
||||
context.withContext { c in
|
||||
UIGraphicsPushContext(c)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
c.clear(CGRect(origin: CGPoint(), size: context.size))
|
||||
contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0)
|
||||
}
|
||||
alphaContext.withContext { c in
|
||||
UIGraphicsPushContext(c)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
c.clear(CGRect(origin: CGPoint(), size: context.size))
|
||||
contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0)
|
||||
}
|
||||
context.blt(alphaContext, at: CGPoint(), mode: .AlphaFromColor)
|
||||
|
||||
self.rasterizedLayer.contents = context.generateImage()?.cgImage
|
||||
}
|
||||
} else {
|
||||
self.rasterizedLayer.contents = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRasterizationMode() {
|
||||
self.maskedLayer.isHidden = !self.contentsLayer.hasAnimationsInTree
|
||||
if self.rasterizedLayer.isHidden != (!self.maskedLayer.isHidden) {
|
||||
self.rasterizedLayer.isHidden = (!self.maskedLayer.isHidden)
|
||||
if !self.rasterizedLayer.isHidden {
|
||||
self.updateContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Improving_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -4628,22 +4628,63 @@ extension ChatControllerImpl {
|
|||
|
||||
if let peerId = peerId {
|
||||
self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] namespace, silent in
|
||||
if let strongSelf = self {
|
||||
let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||||
if inAppNotificationSettings.playSounds && !silent {
|
||||
serviceSoundManager.playMessageDeliveredSound()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] eventGroup in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let inAppNotificationSettings = self.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||||
if inAppNotificationSettings.playSounds, let firstEvent = eventGroup.first, !firstEvent.isSilent {
|
||||
serviceSoundManager.playMessageDeliveredSound()
|
||||
}
|
||||
if self.presentationInterfaceState.subject != .scheduledMessages, let firstEvent = eventGroup.first, firstEvent.id.namespace == Namespaces.Message.ScheduledCloud {
|
||||
if eventGroup.contains(where: { $0.isPendingProcessing }) {
|
||||
self.openScheduledMessages(completion: { [weak self] c in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
c.dismissAllUndoControllers()
|
||||
|
||||
Queue.mainQueue().after(0.5) { [weak c] in
|
||||
c?.displayProcessingVideoTooltip(messageId: firstEvent.id)
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
c.present(
|
||||
UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Chat/ToastImprovingVideo"), color: .white)!,
|
||||
size: nil,
|
||||
title: "Improving video...",
|
||||
text: "The video will be published after it's optimized for the bese viewing experience.",
|
||||
customUndoText: nil,
|
||||
timeout: 6.0
|
||||
),
|
||||
elevatedLayout: false,
|
||||
position: .top,
|
||||
action: { _ in
|
||||
return true
|
||||
}
|
||||
),
|
||||
in: .current
|
||||
)
|
||||
})
|
||||
}
|
||||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud {
|
||||
strongSelf.openScheduledMessages()
|
||||
}
|
||||
|
||||
if self.shouldDisplayChecksTooltip {
|
||||
Queue.mainQueue().after(1.0) { [weak self] in
|
||||
self?.displayChecksTooltip()
|
||||
}
|
||||
|
||||
if strongSelf.shouldDisplayChecksTooltip {
|
||||
Queue.mainQueue().after(1.0) {
|
||||
strongSelf.displayChecksTooltip()
|
||||
}
|
||||
strongSelf.shouldDisplayChecksTooltip = false
|
||||
strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict())
|
||||
self.shouldDisplayChecksTooltip = false
|
||||
self.checksTooltipDisposable.set(self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict())
|
||||
}
|
||||
|
||||
if let shouldDisplayProcessingVideoTooltip = self.shouldDisplayProcessingVideoTooltip {
|
||||
self.shouldDisplayProcessingVideoTooltip = nil
|
||||
Queue.mainQueue().after(1.0) { [weak self] in
|
||||
self?.displayProcessingVideoTooltip(messageId: shouldDisplayProcessingVideoTooltip)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -448,6 +448,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
|
||||
let checksTooltipDisposable = MetaDisposable()
|
||||
var shouldDisplayChecksTooltip = false
|
||||
var shouldDisplayProcessingVideoTooltip: EngineMessage.Id?
|
||||
|
||||
let peerSuggestionsDisposable = MetaDisposable()
|
||||
let peerSuggestionsDismissDisposable = MetaDisposable()
|
||||
|
|
@ -480,6 +481,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
weak var copyProtectionTooltipController: TooltipController?
|
||||
weak var emojiPackTooltipController: TooltipScreen?
|
||||
weak var birthdayTooltipController: TooltipScreen?
|
||||
weak var scheduledVideoProcessingTooltipController: TooltipScreen?
|
||||
|
||||
weak var slowmodeTooltipController: ChatSlowmodeHintController?
|
||||
|
||||
|
|
@ -10357,6 +10359,61 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}
|
||||
}
|
||||
|
||||
func displayProcessingVideoTooltip(messageId: EngineMessage.Id) {
|
||||
self.checksTooltipController?.dismiss()
|
||||
|
||||
var latestNode: ChatMessageItemView?
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
||||
var found = false
|
||||
for (message, _) in item.content {
|
||||
if message.id == messageId {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
latestNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
if let itemNode = latestNode, let statusNode = itemNode.getStatusNode() {
|
||||
let bounds = statusNode.view.convert(statusNode.view.bounds, to: self.chatDisplayNode.view)
|
||||
let location = CGPoint(x: bounds.midX, y: bounds.minY - 11.0)
|
||||
|
||||
//TODO:localize
|
||||
let tooltipController = TooltipController(content: .text("Processing video may take a few minutes."), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||||
self.checksTooltipController = tooltipController
|
||||
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
|
||||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController {
|
||||
strongSelf.checksTooltipController = nil
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.chatDisplayNode.messageTransitionNode.addCustomOffsetHandler(itemNode: itemNode, update: { [weak tooltipController] offset, transition in
|
||||
guard let tooltipController, tooltipController.isNodeLoaded else {
|
||||
return false
|
||||
}
|
||||
guard let containerView = tooltipController.view else {
|
||||
return false
|
||||
}
|
||||
containerView.bounds = containerView.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateOffsetAdditive(layer: containerView.layer, offset: offset)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return (self.chatDisplayNode, CGRect(origin: location, size: CGSize()))
|
||||
}, sourceRectIsGlobal: true))
|
||||
}
|
||||
}
|
||||
|
||||
func dismissAllTooltips() {
|
||||
self.emojiTooltipController?.dismiss()
|
||||
self.sendingOptionsTooltipController?.dismiss()
|
||||
|
|
@ -10537,10 +10594,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages)
|
||||
controller.navigationPresentation = .modal
|
||||
navigationController.pushViewController(controller, completion: { [weak controller] in
|
||||
if let controller {
|
||||
let _ = controller
|
||||
/*if let controller {
|
||||
completion(controller)
|
||||
}
|
||||
}*/
|
||||
})
|
||||
completion(controller)
|
||||
}
|
||||
|
||||
func openPinnedMessages(at messageId: MessageId?) {
|
||||
|
|
|
|||
|
|
@ -1600,6 +1600,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
|
||||
}
|
||||
|
||||
func enterNativePictureInPicture() -> Bool {
|
||||
|
|
@ -1608,6 +1609,10 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
self.imageNode.isHidden = !value
|
||||
}
|
||||
}
|
||||
|
||||
private func serializeRanges(_ ranges: RangeSet<Double>) -> [Double] {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||
public let nativeId: NativeVideoContentId
|
||||
public let userLocation: MediaResourceUserLocation
|
||||
public let fileReference: FileMediaReference
|
||||
public let previewSourceFileReference: FileMediaReference?
|
||||
public let limitedFileRange: Range<Int64>?
|
||||
let imageReference: ImageMediaReference?
|
||||
public let dimensions: CGSize
|
||||
|
|
@ -109,11 +110,12 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||
return file
|
||||
}
|
||||
|
||||
public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, limitedFileRange: Range<Int64>? = nil, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) {
|
||||
public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, limitedFileRange: Range<Int64>? = nil, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) {
|
||||
self.id = id
|
||||
self.nativeId = id
|
||||
self.userLocation = userLocation
|
||||
self.fileReference = fileReference
|
||||
self.previewSourceFileReference = previewSourceFileReference
|
||||
self.limitedFileRange = limitedFileRange
|
||||
self.imageReference = imageReference
|
||||
if var dimensions = fileReference.media.dimensions {
|
||||
|
|
@ -156,7 +158,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||
}
|
||||
|
||||
public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay)
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, previewSourceFileReference: self.previewSourceFileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay)
|
||||
}
|
||||
|
||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||
|
|
@ -177,6 +179,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
private let postbox: Postbox
|
||||
private let userLocation: MediaResourceUserLocation
|
||||
private let fileReference: FileMediaReference
|
||||
private let previewSourceFileReference: FileMediaReference?
|
||||
private let limitedFileRange: Range<Int64>?
|
||||
private let streamVideo: MediaPlayerStreaming
|
||||
private let enableSound: Bool
|
||||
|
|
@ -246,10 +249,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
|
||||
private let hasSentFramesToDisplay: (() -> Void)?
|
||||
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, limitedFileRange: Range<Int64>?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) {
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, limitedFileRange: Range<Int64>?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) {
|
||||
self.postbox = postbox
|
||||
self.userLocation = userLocation
|
||||
self.fileReference = fileReference
|
||||
self.previewSourceFileReference = previewSourceFileReference
|
||||
self.limitedFileRange = limitedFileRange
|
||||
self.streamVideo = streamVideo
|
||||
self.placeholderColor = placeholderColor
|
||||
|
|
@ -324,7 +328,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
setLayerDisableScreenshots(self.imageNode.layer, captureProtected)
|
||||
}
|
||||
|
||||
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in
|
||||
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, previewSourceFileReference: previewSourceFileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self, strongSelf.dimensions == nil {
|
||||
if let dimensions = getSize() {
|
||||
|
|
@ -716,4 +720,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
self.imageNode.isHidden = value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,4 +482,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,5 +319,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -234,4 +234,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,4 +218,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
|||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
|
||||
func setNativePictureInPictureIsActive(_ value: Bool) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public enum UndoOverlayContent {
|
|||
case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
|
||||
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)
|
||||
case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?)
|
||||
case universalImage(image: UIImage, size: CGSize?, title: String?, text: String, customUndoText: String?, timeout: Double?)
|
||||
case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?)
|
||||
case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?)
|
||||
case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, customUndoText: String?)
|
||||
|
|
|
|||
|
|
@ -1066,6 +1066,56 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||
|
||||
self.textNode.maximumNumberOfLines = 5
|
||||
|
||||
if let customUndoText = customUndoText {
|
||||
undoText = customUndoText
|
||||
displayUndo = true
|
||||
} else {
|
||||
displayUndo = false
|
||||
}
|
||||
case let .universalImage(image, size, title, text, customUndoText, timeout):
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode?.displayWithoutProcessing = true
|
||||
self.iconNode?.displaysAsynchronously = false
|
||||
self.iconNode?.image = image
|
||||
self.iconImageSize = size
|
||||
|
||||
self.avatarNode = nil
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = nil
|
||||
self.animatedStickerNode = nil
|
||||
|
||||
if let title = title, text.isEmpty {
|
||||
self.titleNode.attributedText = nil
|
||||
let body = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
} else {
|
||||
if let title = title {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
} else {
|
||||
self.titleNode.attributedText = nil
|
||||
}
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
}
|
||||
|
||||
if text.contains("](") {
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
self.originalRemainingSeconds = timeout ?? (isUserInteractionEnabled ? 5 : 3)
|
||||
|
||||
self.textNode.maximumNumberOfLines = 5
|
||||
|
||||
if let customUndoText = customUndoText {
|
||||
undoText = customUndoText
|
||||
displayUndo = true
|
||||
|
|
@ -1284,7 +1334,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||
} else {
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged:
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal,. universalImage, .premiumPaywall, .peers, .messageTagged:
|
||||
if self.textNode.tapAttributeAction != nil || displayUndo {
|
||||
self.isUserInteractionEnabled = true
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue