mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
6c57587c2e
commit
caf10fe889
@ -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) {
|
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 {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
completion?(true)
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
view.layer.animate(
|
layer.animate(
|
||||||
from: fromValue as NSNumber,
|
from: fromValue as NSNumber,
|
||||||
to: toValue as NSNumber,
|
to: toValue as NSNumber,
|
||||||
keyPath: "transform.scale",
|
keyPath: "transform.scale",
|
||||||
|
@ -476,6 +476,7 @@ public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloa
|
|||||||
|
|
||||||
public enum DrawingContextBltMode {
|
public enum DrawingContextBltMode {
|
||||||
case Alpha
|
case Alpha
|
||||||
|
case AlphaFromColor
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings {
|
public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings {
|
||||||
@ -774,6 +775,32 @@ public class DrawingContext {
|
|||||||
sx += 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
|
dstY += 1
|
||||||
srcY += 1
|
srcY += 1
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||||
"//submodules/UrlHandling",
|
"//submodules/UrlHandling",
|
||||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||||
|
"//submodules/TelegramUI/Components/RasterizedCompositionComponent",
|
||||||
|
"//submodules/TelegramUI/Components/BadgeComponent",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -29,6 +29,9 @@ import AdsInfoScreen
|
|||||||
import AdsReportScreen
|
import AdsReportScreen
|
||||||
import SaveProgressScreen
|
import SaveProgressScreen
|
||||||
import SectionTitleContextItem
|
import SectionTitleContextItem
|
||||||
|
import RasterizedCompositionComponent
|
||||||
|
import BadgeComponent
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
public enum UniversalVideoGalleryItemContentInfo {
|
public enum UniversalVideoGalleryItemContentInfo {
|
||||||
case message(Message, Int?)
|
case message(Message, Int?)
|
||||||
@ -507,8 +510,19 @@ final class MoreHeaderButton: HighlightableButtonNode {
|
|||||||
final class SettingsHeaderButton: HighlightableButtonNode {
|
final class SettingsHeaderButton: HighlightableButtonNode {
|
||||||
let referenceNode: ContextReferenceContentNode
|
let referenceNode: ContextReferenceContentNode
|
||||||
let containerNode: ContextControllerSourceNode
|
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
|
private var isMenuOpen: Bool = false
|
||||||
|
|
||||||
@ -523,23 +537,24 @@ final class SettingsHeaderButton: HighlightableButtonNode {
|
|||||||
self.containerNode = ContextControllerSourceNode()
|
self.containerNode = ContextControllerSourceNode()
|
||||||
self.containerNode.animateScale = false
|
self.containerNode.animateScale = false
|
||||||
|
|
||||||
self.iconNode = ASImageNode()
|
self.iconLayer = RasterizedCompositionMonochromeLayer()
|
||||||
self.iconNode.displaysAsynchronously = false
|
//self.iconLayer.backgroundColor = UIColor.green.cgColor
|
||||||
self.iconNode.displayWithoutProcessing = true
|
|
||||||
self.iconNode.contentMode = .scaleToFill
|
|
||||||
|
|
||||||
self.iconDotNode = ASImageNode()
|
self.gearsLayer = RasterizedCompositionImageLayer()
|
||||||
self.iconDotNode.displaysAsynchronously = false
|
self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white)
|
||||||
self.iconDotNode.displayWithoutProcessing = true
|
|
||||||
|
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()
|
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.containerNode.addSubnode(self.referenceNode)
|
||||||
self.referenceNode.addSubnode(self.iconNode)
|
self.referenceNode.layer.addSublayer(self.iconLayer)
|
||||||
self.referenceNode.addSubnode(self.iconDotNode)
|
|
||||||
self.addSubnode(self.containerNode)
|
self.addSubnode(self.containerNode)
|
||||||
|
|
||||||
self.containerNode.shouldBegin = { [weak self] location in
|
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)
|
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
|
||||||
|
|
||||||
if let image = self.iconNode.image {
|
if let image = self.gearsLayer.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)
|
let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0)
|
||||||
self.iconNode.position = iconFrame.center
|
let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom)
|
||||||
self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
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 {
|
self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
|
||||||
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.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||||
self.iconDotNode.position = dotFrame.center
|
|
||||||
self.iconDotNode.bounds = CGRect(origin: CGPoint(), size: dotFrame.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() {
|
override func didLoad() {
|
||||||
@ -592,21 +622,111 @@ final class SettingsHeaderButton: HighlightableButtonNode {
|
|||||||
self.isMenuOpen = isMenuOpen
|
self.isMenuOpen = isMenuOpen
|
||||||
|
|
||||||
let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
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))
|
rotationTransition.updateTransform(layer: self.gearsLayer, 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
|
self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
guard let self, finished else {
|
guard let self, finished else {
|
||||||
return
|
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 {
|
guard let self, finished else {
|
||||||
return
|
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.0, left: 1.66, bottom: 1.0, right: 1.0),
|
||||||
|
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, *)
|
@available(iOS 15.0, *)
|
||||||
@ -1201,6 +1321,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private var playbackRate: Double?
|
private var playbackRate: Double?
|
||||||
private var videoQuality: UniversalVideoContentVideoQuality = .auto
|
private var videoQuality: UniversalVideoContentVideoQuality = .auto
|
||||||
private let playbackRatePromise = ValuePromise<Double>()
|
private let playbackRatePromise = ValuePromise<Double>()
|
||||||
|
private let videoQualityPromise = ValuePromise<UniversalVideoContentVideoQuality>()
|
||||||
|
|
||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
private let moreButtonStateDisposable = MetaDisposable()
|
private let moreButtonStateDisposable = MetaDisposable()
|
||||||
@ -1705,46 +1826,38 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
|
self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
|
||||||
self.playbackRatePromise.get(),
|
self.playbackRatePromise.get(),
|
||||||
self.isShowingContextMenuPromise.get()
|
self.videoQualityPromise.get()
|
||||||
).start(next: { [weak self] playbackRate, isShowingContextMenu in
|
).start(next: { [weak self] playbackRate, videoQuality in
|
||||||
guard let strongSelf = self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let effectiveBaseRate: Double
|
var rateString: String?
|
||||||
if isShowingContextMenu {
|
if abs(playbackRate - 1.0) > 0.1 {
|
||||||
effectiveBaseRate = 1.0
|
var stringValue = String(format: "%.1fx", playbackRate)
|
||||||
} else {
|
|
||||||
effectiveBaseRate = playbackRate
|
|
||||||
}
|
|
||||||
|
|
||||||
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") {
|
if stringValue.hasSuffix(".0x") {
|
||||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||||
}
|
}
|
||||||
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated)
|
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 {
|
} else {
|
||||||
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
|
qualityString = "UHD"
|
||||||
}
|
|
||||||
} 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()
|
self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get()
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in
|
|> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in
|
||||||
@ -1996,6 +2109,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
||||||
|
self.videoQualityPromise.set(self.videoQuality)
|
||||||
|
|
||||||
var isAd = false
|
var isAd = false
|
||||||
if let contentInfo = item.contentInfo {
|
if let contentInfo = item.contentInfo {
|
||||||
@ -3333,8 +3447,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
videoNode.setVideoQuality(.auto)
|
videoNode.setVideoQuality(.auto)
|
||||||
//TODO:release
|
self.videoQualityPromise.set(.auto)
|
||||||
//self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
|
|
||||||
|
|
||||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||||
controller.updateSharedPlaybackRate(rate)
|
controller.updateSharedPlaybackRate(rate)
|
||||||
@ -3367,12 +3480,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
videoNode.setVideoQuality(.quality(quality))
|
videoNode.setVideoQuality(.quality(quality))
|
||||||
//TODO:release
|
self.videoQualityPromise.set(.quality(quality))
|
||||||
/*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)))
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||||
controller.updateSharedPlaybackRate(rate)
|
controller.updateSharedPlaybackRate(rate)
|
||||||
@ -3407,15 +3515,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
c?.popItems()
|
c?.popItems()
|
||||||
})))
|
})))
|
||||||
|
|
||||||
for quality in qualityState.available {
|
let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in
|
||||||
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guard let qualityFileSize = qualityFile.media.size else {
|
guard let qualityFileSize = qualityFile.media.size else {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
|
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
|
return nil
|
||||||
}, action: { [weak self] c, _ in
|
}, action: { [weak self] c, _ in
|
||||||
c?.dismiss(result: .default, completion: nil)
|
c?.dismiss(result: .default, completion: nil)
|
||||||
@ -3458,6 +3569,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))))
|
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
|
||||||
} else {
|
} else {
|
||||||
c?.dismiss(result: .default, completion: nil)
|
c?.dismiss(result: .default, completion: nil)
|
||||||
@ -3683,6 +3809,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||||
self.videoQuality = videoQuality
|
self.videoQuality = videoQuality
|
||||||
|
self.videoQualityPromise.set(videoQuality)
|
||||||
|
|
||||||
self.videoNode?.setVideoQuality(videoQuality)
|
self.videoNode?.setVideoQuality(videoQuality)
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ private var declaredEncodables: Void = {
|
|||||||
declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) })
|
declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) })
|
||||||
declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) })
|
declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) })
|
||||||
declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(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(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
|
||||||
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
|
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
|
||||||
declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(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) {
|
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||||
switch apiMessage {
|
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):
|
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
|
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
|
||||||
|
|
||||||
var namespace = namespace
|
var namespace = namespace
|
||||||
@ -676,8 +682,6 @@ extension StoreMessage {
|
|||||||
authorId = resolvedFromId
|
authorId = resolvedFromId
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributes: [MessageAttribute] = []
|
|
||||||
|
|
||||||
var threadId: Int64?
|
var threadId: Int64?
|
||||||
if let replyTo = replyTo {
|
if let replyTo = replyTo {
|
||||||
var threadMessageId: MessageId?
|
var threadMessageId: MessageId?
|
||||||
|
@ -258,7 +258,8 @@ private final class PendingPeerMediaUploadManagerImpl {
|
|||||||
message: message,
|
message: message,
|
||||||
cacheReferenceKey: nil,
|
cacheReferenceKey: nil,
|
||||||
result: result,
|
result: result,
|
||||||
accountPeerId: accountPeerId
|
accountPeerId: accountPeerId,
|
||||||
|
pendingMessageEvent: { _ in }
|
||||||
)
|
)
|
||||||
|> deliverOn(queue)).start(completed: { [weak self, weak context] in
|
|> deliverOn(queue)).start(completed: { [weak self, weak context] in
|
||||||
guard let strongSelf = self, let initialContext = context else {
|
guard let strongSelf = self, let initialContext = context else {
|
||||||
|
@ -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
|
return postbox.transaction { transaction -> Void in
|
||||||
let messageId: Int32?
|
let messageId: Int32?
|
||||||
var apiMessage: Api.Message?
|
var apiMessage: Api.Message?
|
||||||
@ -125,7 +125,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||||||
var sentStickers: [TelegramMediaFile] = []
|
var sentStickers: [TelegramMediaFile] = []
|
||||||
var sentGifs: [TelegramMediaFile] = []
|
var sentGifs: [TelegramMediaFile] = []
|
||||||
|
|
||||||
if let updatedTimestamp = updatedTimestamp {
|
if let updatedTimestamp {
|
||||||
transaction.offsetPendingMessagesTimestamps(lowerBound: message.id, excludeIds: Set([message.id]), timestamp: 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] = []
|
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
|
||||||
|
|
||||||
transaction.updateMessage(message.id, update: { currentMessage in
|
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]
|
let media: [Media]
|
||||||
var attributes: [MessageAttribute]
|
var attributes: [MessageAttribute]
|
||||||
let text: String
|
let text: String
|
||||||
@ -195,14 +178,12 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||||||
updatedAttributes.append(MediaSpoilerMessageAttribute())
|
updatedAttributes.append(MediaSpoilerMessageAttribute())
|
||||||
}
|
}
|
||||||
|
|
||||||
if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud {
|
|
||||||
for i in 0 ..< updatedAttributes.count {
|
for i in 0 ..< updatedAttributes.count {
|
||||||
if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute {
|
if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute {
|
||||||
updatedAttributes.remove(at: i)
|
updatedAttributes.remove(at: i)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
|
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
|
||||||
for i in 0 ..< updatedAttributes.count {
|
for i in 0 ..< updatedAttributes.count {
|
||||||
if updatedAttributes[i] is OutgoingQuickReplyMessageAttribute {
|
if updatedAttributes[i] is OutgoingQuickReplyMessageAttribute {
|
||||||
@ -225,6 +206,30 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||||||
threadId = currentMessage.threadId
|
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 {
|
for attribute in currentMessage.attributes {
|
||||||
if let attribute = attribute as? OutgoingMessageInfoAttribute {
|
if let attribute = attribute as? OutgoingMessageInfoAttribute {
|
||||||
bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets
|
bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets
|
||||||
@ -358,12 +363,26 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||||||
|
|
||||||
stateManager.addUpdates(result)
|
stateManager.addUpdates(result)
|
||||||
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: message.id.peerId)])
|
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 {
|
guard !messages.isEmpty else {
|
||||||
return .complete()
|
return .single(Void())
|
||||||
}
|
}
|
||||||
|
|
||||||
return postbox.transaction { transaction -> Void in
|
return postbox.transaction { transaction -> Void in
|
||||||
@ -372,8 +391,12 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
|||||||
var namespace = Namespaces.Message.Cloud
|
var namespace = Namespaces.Message.Cloud
|
||||||
if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) {
|
if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) {
|
||||||
namespace = Namespaces.Message.QuickReplyCloud
|
namespace = Namespaces.Message.QuickReplyCloud
|
||||||
} else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
} else if let message = messages.first, let apiMessage = result.messages.first {
|
||||||
|
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||||
namespace = Namespaces.Message.ScheduledCloud
|
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] = [:]
|
var resultMessages: [MessageId: StoreMessage] = [:]
|
||||||
@ -538,6 +561,23 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
|||||||
}
|
}
|
||||||
stateManager.addUpdates(result)
|
stateManager.addUpdates(result)
|
||||||
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: messages[0].id.peerId)])
|
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 {
|
private final class PeerPendingMessagesSummaryContext {
|
||||||
var messageDeliveredSubscribers = Bag<((MessageId.Namespace, Bool)) -> Void>()
|
var messageDeliveredSubscribers = Bag<([PeerPendingMessageDelivered]) -> Void>()
|
||||||
var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>()
|
var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,29 +282,32 @@ public final class PendingMessageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !removedSecretMessageIds.isEmpty {
|
if !removedSecretMessageIds.isEmpty {
|
||||||
let _ = (self.postbox.transaction { transaction -> (Set<PeerId>, Bool) in
|
let _ = (self.postbox.transaction { transaction -> [PeerId: [PeerPendingMessageDelivered]] in
|
||||||
var silent = false
|
var peerIdsWithDeliveredMessages: [PeerId: [PeerPendingMessageDelivered]] = [:]
|
||||||
var peerIdsWithDeliveredMessages = Set<PeerId>()
|
|
||||||
for id in removedSecretMessageIds {
|
for id in removedSecretMessageIds {
|
||||||
if let message = transaction.getMessage(id) {
|
if let message = transaction.getMessage(id) {
|
||||||
if message.isSentOrAcknowledged {
|
if message.isSentOrAcknowledged {
|
||||||
peerIdsWithDeliveredMessages.insert(id.peerId)
|
var silent = false
|
||||||
if message.muted {
|
if message.muted {
|
||||||
silent = true
|
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 {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for peerId in peerIdsWithDeliveredMessages {
|
for (peerId, deliveredMessages) in peerIdsWithDeliveredMessages {
|
||||||
if let context = strongSelf.peerSummaryContexts[peerId] {
|
if let context = strongSelf.peerSummaryContexts[peerId] {
|
||||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
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 {
|
if message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
||||||
namespace = Namespaces.Message.QuickReplyCloud
|
} else if let apiMessage {
|
||||||
} else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) {
|
var isScheduled = false
|
||||||
namespace = id.namespace
|
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
||||||
|
isScheduled = true
|
||||||
|
}
|
||||||
|
if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage {
|
||||||
|
if (flags2 & (1 << 4)) != 0 {
|
||||||
|
isScheduled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId {
|
||||||
self.correlationIdToSentMessageId.with { value in
|
self.correlationIdToSentMessageId.with { value in
|
||||||
value.mapping[correlationId] = id
|
value.mapping[correlationId] = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId)
|
let queue = self.queue
|
||||||
|> afterDisposed { [weak self] in
|
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 strongSelf = self {
|
||||||
strongSelf.queue.async {
|
|
||||||
if let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
if let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
||||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
||||||
subscriber((namespace, silent))
|
subscriber([pendingMessageDelivered])
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal<Void, NoError> {
|
private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal<Void, NoError> {
|
||||||
var silent = false
|
|
||||||
var namespace = Namespaces.Message.Cloud
|
var namespace = Namespaces.Message.Cloud
|
||||||
if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
if let message = messages.first {
|
||||||
|
if message.id.namespace == Namespaces.Message.QuickReplyLocal {
|
||||||
namespace = Namespaces.Message.QuickReplyCloud
|
namespace = Namespaces.Message.QuickReplyCloud
|
||||||
} else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
|
} 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
|
namespace = Namespaces.Message.ScheduledCloud
|
||||||
if message.muted {
|
|
||||||
silent = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1777,22 +1798,22 @@ public final class PendingMessageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let queue = self.queue
|
||||||
|
|
||||||
return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result)
|
return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result, pendingMessageEvents: { [weak self] pendingMessagesDelivered in
|
||||||
|> afterDisposed { [weak self] in
|
queue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.queue.async {
|
if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId], !pendingMessagesDelivered.isEmpty {
|
||||||
if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId] {
|
|
||||||
for subscriber in context.messageDeliveredSubscribers.copyItems() {
|
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
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
@ -1805,8 +1826,8 @@ public final class PendingMessageManager {
|
|||||||
self.peerSummaryContexts[peerId] = summaryContext
|
self.peerSummaryContexts[peerId] = summaryContext
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = summaryContext.messageDeliveredSubscribers.add({ namespace, silent in
|
let index = summaryContext.messageDeliveredSubscribers.add({ event in
|
||||||
subscriber.putNext((namespace, silent))
|
subscriber.putNext(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
disposable.set(ActionDisposable {
|
disposable.set(ActionDisposable {
|
||||||
|
@ -115,7 +115,11 @@ extension Api.Message {
|
|||||||
|
|
||||||
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
|
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
|
||||||
switch self {
|
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
|
let peerId: PeerId = messagePeerId.peerId
|
||||||
return MessageId(peerId: peerId, namespace: namespace, id: id)
|
return MessageId(peerId: peerId, namespace: namespace, id: id)
|
||||||
case let .messageEmpty(_, id, peerId):
|
case let .messageEmpty(_, id, peerId):
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
import Postbox
|
||||||
|
|
||||||
public class WasScheduledMessageAttribute: MessageAttribute {
|
public class PendingProcessingMessageAttribute: MessageAttribute {
|
||||||
public init() {
|
public let approximateCompletionTime: Int32
|
||||||
|
|
||||||
|
public init(approximateCompletionTime: Int32) {
|
||||||
|
self.approximateCompletionTime = approximateCompletionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(decoder: PostboxDecoder) {
|
required public init(decoder: PostboxDecoder) {
|
||||||
|
self.approximateCompletionTime = decoder.decodeInt32ForKey("et", orElse: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(_ encoder: PostboxEncoder) {
|
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 {
|
public extension Message {
|
||||||
func areReactionsTags(accountPeerId: PeerId) -> Bool {
|
func areReactionsTags(accountPeerId: PeerId) -> Bool {
|
||||||
if self.id.peerId == accountPeerId {
|
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,135 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size))
|
||||||
|
let textImage = renderer.image { context in
|
||||||
|
UIGraphicsPushContext(context.cgContext)
|
||||||
|
attributedText.draw(at: CGPoint())
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
}
|
||||||
|
self.textContentsLayer.image = textImage
|
||||||
|
}
|
||||||
|
|
||||||
|
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.textContentsLayer.image?.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)
|
||||||
|
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: component.insets.left, y: component.insets.top), size: textSize)
|
||||||
|
transition.setPosition(layer: self.textContentsLayer, position: textFrame.origin)
|
||||||
|
self.textContentsLayer.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||||
|
//self.textContentsLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.4).cgColor
|
||||||
|
|
||||||
|
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 = " "
|
dateText = " "
|
||||||
}
|
}
|
||||||
|
|
||||||
/*if "".isEmpty, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute {
|
||||||
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)"
|
return "appx. \(dateText)"
|
||||||
} else if message.id.namespace == Namespaces.Message.ScheduledLocal {
|
|
||||||
return "processing"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if displayFullDate {
|
if displayFullDate {
|
||||||
let dayText: String
|
let dayText: String
|
||||||
|
@ -110,7 +110,7 @@ private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func dateHeaderTimestampId(timestamp: Int32) -> Int32 {
|
private func dateHeaderTimestampId(timestamp: Int32) -> Int32 {
|
||||||
if timestamp == scheduleWhenOnlineTimestamp {
|
if timestamp == scheduleWhenOnlineTimestamp || timestamp >= Int32.max - 1000 {
|
||||||
return timestamp
|
return timestamp
|
||||||
} else if timestamp == Int32.max {
|
} else if timestamp == Int32.max {
|
||||||
return timestamp / (granularity) * (granularity)
|
return timestamp / (granularity) * (granularity)
|
||||||
|
@ -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 {
|
if let peerId = peerId {
|
||||||
self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)
|
self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] namespace, silent in
|
|> deliverOnMainQueue).startStrict(next: { [weak self] eventGroup in
|
||||||
if let strongSelf = self {
|
guard let self else {
|
||||||
let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
return
|
||||||
if inAppNotificationSettings.playSounds && !silent {
|
}
|
||||||
|
let inAppNotificationSettings = self.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||||||
|
if inAppNotificationSettings.playSounds, let firstEvent = eventGroup.first, !firstEvent.isSilent {
|
||||||
serviceSoundManager.playMessageDeliveredSound()
|
serviceSoundManager.playMessageDeliveredSound()
|
||||||
}
|
}
|
||||||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud {
|
if self.presentationInterfaceState.subject != .scheduledMessages, let firstEvent = eventGroup.first, firstEvent.id.namespace == Namespaces.Message.ScheduledCloud {
|
||||||
strongSelf.openScheduledMessages()
|
if eventGroup.contains(where: { $0.isPendingProcessing }) {
|
||||||
|
self.openScheduledMessages(completion: { [weak self] c in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.shouldDisplayChecksTooltip {
|
c.dismissAllUndoControllers()
|
||||||
Queue.mainQueue().after(1.0) {
|
|
||||||
strongSelf.displayChecksTooltip()
|
Queue.mainQueue().after(1.0) { [weak c] in
|
||||||
|
c?.displayProcessingVideoTooltip(messageId: firstEvent.id)
|
||||||
}
|
}
|
||||||
strongSelf.shouldDisplayChecksTooltip = false
|
|
||||||
strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict())
|
//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 self.shouldDisplayChecksTooltip {
|
||||||
|
Queue.mainQueue().after(1.0) { [weak self] in
|
||||||
|
self?.displayChecksTooltip()
|
||||||
|
}
|
||||||
|
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()
|
let checksTooltipDisposable = MetaDisposable()
|
||||||
var shouldDisplayChecksTooltip = false
|
var shouldDisplayChecksTooltip = false
|
||||||
|
var shouldDisplayProcessingVideoTooltip: EngineMessage.Id?
|
||||||
|
|
||||||
let peerSuggestionsDisposable = MetaDisposable()
|
let peerSuggestionsDisposable = MetaDisposable()
|
||||||
let peerSuggestionsDismissDisposable = MetaDisposable()
|
let peerSuggestionsDismissDisposable = MetaDisposable()
|
||||||
@ -10350,6 +10351,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func displayProcessingVideoTooltip(messageId: EngineMessage.Id) {
|
||||||
|
self.checksTooltipController?.dismiss()
|
||||||
|
|
||||||
|
var latestNode: (Int32, ASDisplayNode)?
|
||||||
|
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||||
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let statusNode = itemNode.getStatusNode() {
|
||||||
|
var found = false
|
||||||
|
for (message, _) in item.content {
|
||||||
|
if message.id == messageId {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !item.content.effectivelyIncoming(self.context.account.peerId) {
|
||||||
|
if let (latestTimestamp, _) = latestNode {
|
||||||
|
if item.message.timestamp > latestTimestamp {
|
||||||
|
latestNode = (item.message.timestamp, statusNode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
latestNode = (item.message.timestamp, statusNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (_, latestStatusNode) = latestNode {
|
||||||
|
let bounds = latestStatusNode.view.convert(latestStatusNode.view.bounds, to: self.chatDisplayNode.view)
|
||||||
|
let location = CGPoint(x: bounds.maxX - 7.0, y: bounds.minY - 11.0)
|
||||||
|
|
||||||
|
let contentNode = ChatStatusChecksTooltipContentNode(presentationData: self.presentationData)
|
||||||
|
let tooltipController = TooltipController(content: .custom(contentNode), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
return (strongSelf.chatDisplayNode, CGRect(origin: location, size: CGSize()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func dismissAllTooltips() {
|
func dismissAllTooltips() {
|
||||||
self.emojiTooltipController?.dismiss()
|
self.emojiTooltipController?.dismiss()
|
||||||
self.sendingOptionsTooltipController?.dismiss()
|
self.sendingOptionsTooltipController?.dismiss()
|
||||||
@ -10530,10 +10580,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages)
|
let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages)
|
||||||
controller.navigationPresentation = .modal
|
controller.navigationPresentation = .modal
|
||||||
navigationController.pushViewController(controller, completion: { [weak controller] in
|
navigationController.pushViewController(controller, completion: { [weak controller] in
|
||||||
if let controller {
|
let _ = controller
|
||||||
|
/*if let controller {
|
||||||
completion(controller)
|
completion(controller)
|
||||||
}
|
}*/
|
||||||
})
|
})
|
||||||
|
completion(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openPinnedMessages(at messageId: MessageId?) {
|
func openPinnedMessages(at messageId: MessageId?) {
|
||||||
|
@ -45,6 +45,7 @@ public enum UndoOverlayContent {
|
|||||||
case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
|
case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
|
||||||
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)
|
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 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 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 peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?)
|
||||||
case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, 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
|
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 {
|
if let customUndoText = customUndoText {
|
||||||
undoText = customUndoText
|
undoText = customUndoText
|
||||||
displayUndo = true
|
displayUndo = true
|
||||||
@ -1284,7 +1334,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
} else {
|
} else {
|
||||||
self.isUserInteractionEnabled = false
|
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 {
|
if self.textNode.tapAttributeAction != nil || displayUndo {
|
||||||
self.isUserInteractionEnabled = true
|
self.isUserInteractionEnabled = true
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user