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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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.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, *)
|
||||
@ -1201,6 +1321,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()
|
||||
@ -1705,46 +1826,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 +2109,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 {
|
||||
@ -3333,8 +3447,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 +3480,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 +3515,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 +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))))
|
||||
} else {
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
@ -3683,6 +3809,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||
self.videoQuality = videoQuality
|
||||
self.videoQualityPromise.set(videoQuality)
|
||||
|
||||
self.videoNode?.setVideoQuality(videoQuality)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,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 = " "
|
||||
}
|
||||
|
||||
/*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
|
||||
|
@ -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)
|
||||
|
@ -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(1.0) { [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()
|
||||
@ -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() {
|
||||
self.emojiTooltipController?.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)
|
||||
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?) {
|
||||
|
@ -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…
x
Reference in New Issue
Block a user