Various improvements

This commit is contained in:
Isaac 2024-10-25 11:24:06 +02:00
parent 6c57587c2e
commit caf10fe889
24 changed files with 1175 additions and 216 deletions

View File

@ -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",

View File

@ -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
} }

View File

@ -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",

View File

@ -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)
} }

View File

@ -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) })

View File

@ -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?

View File

@ -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 {

View File

@ -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 })
)
})
} }
} }

View File

@ -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 {

View File

@ -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):

View File

@ -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")
} }
} }

View File

@ -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 {

View 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",
],
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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",
],
)

View File

@ -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()
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Improving_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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)
} }
} }
})) }))

View File

@ -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?) {

View File

@ -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?)

View File

@ -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 {