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) {
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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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 {
func areReactionsTags(accountPeerId: PeerId) -> Bool {
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 = " "
}
/*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

View File

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

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

View File

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

View File

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

View File

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