diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4c16d038c0..30046f25db 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -2891,6 +2891,7 @@ Unused sets are archived when you add more."; "FastTwoStepSetup.EmailHelp" = "Please add your valid e-mail. It is the only way to recover a forgotten password."; "Conversation.ViewMessage" = "VIEW MESSAGE"; +"Conversation.ViewPost" = "VIEW POST"; "GroupInfo.GroupHistory" = "History For New Members"; "GroupInfo.GroupHistoryVisible" = "Visible"; diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 361e6f2f46..ec099f974b 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -12,14 +12,28 @@ import PhotoResources import DirectMediaImageCache import TelegramStringFormatting -private final class MediaPreviewView: UIView { +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +private class SimpleLayer: CALayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } + + func update(size: CGSize) { + } +} + +private final class MediaPreviewView: SimpleLayer { private let context: AccountContext private let message: EngineMessage private let media: EngineMedia private let imageCache: DirectMediaImageCache - private let imageView: UIImageView - private var requestedImage: Bool = false private var disposable: Disposable? @@ -29,12 +43,9 @@ private final class MediaPreviewView: UIView { self.media = media self.imageCache = imageCache - self.imageView = UIImageView() - self.imageView.contentMode = .scaleToFill + super.init() - super.init(frame: CGRect()) - - self.addSubview(self.imageView) + self.contentsGravity = .resize } required init?(coder: NSCoder) { @@ -62,7 +73,7 @@ private final class MediaPreviewView: UIView { self.requestedImage = true if let result = self.imageCache.getImage(message: self.message._asMessage(), media: self.media._asMedia(), width: 100, possibleWidths: [100], synchronous: false) { if let image = result.image { - self.imageView.image = processImage(image) + self.contents = processImage(image).cgImage } if let signal = result.loadSignal { self.disposable = (signal @@ -74,49 +85,22 @@ private final class MediaPreviewView: UIView { return } if let image = image { - if strongSelf.imageView.image != nil { - let tempView = UIImageView() - tempView.image = strongSelf.imageView.image - tempView.frame = strongSelf.imageView.frame - tempView.contentMode = strongSelf.imageView.contentMode - strongSelf.addSubview(tempView) - tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in - tempView?.removeFromSuperview() + if strongSelf.contents != nil { + let tempView = SimpleLayer() + tempView.contents = strongSelf.contents + tempView.frame = strongSelf.bounds + tempView.contentsGravity = strongSelf.contentsGravity + strongSelf.addSublayer(tempView) + tempView.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in + tempView?.removeFromSuperlayer() }) } - strongSelf.imageView.image = image + strongSelf.contents = image.cgImage } }) } } } - - self.imageView.frame = CGRect(origin: CGPoint(), size: size) - /*var dimensions = CGSize(width: 100.0, height: 100.0) - if case let .image(image) = self.media { - if let largest = largestImageRepresentation(image.representations) { - dimensions = largest.dimensions.cgSize - if !self.requestedImage { - self.requestedImage = true - let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads) - self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) - } - } - } else if case let .file(file) = self.media { - if let mediaDimensions = file.dimensions { - dimensions = mediaDimensions.cgSize - if !self.requestedImage { - self.requestedImage = true - let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true) - self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) - } - } - } - - let makeLayout = self.imageView.asyncLayout() - self.imageView.frame = CGRect(origin: CGPoint(), size: size) - let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) - apply()*/ } } @@ -425,12 +409,10 @@ private final class DayComponent: Component { return true } - final class View: UIView { - private let button: HighlightTrackingButton - - private let highlightView: UIImageView - private var selectionView: UIImageView? - private let titleView: UIImageView + final class View: HighlightTrackingButton { + private let highlightView: SimpleLayer + private var selectionView: SimpleLayer? + private let titleView: SimpleLayer private var mediaPreviewView: MediaPreviewView? private var action: (() -> Void)? @@ -441,29 +423,24 @@ private final class DayComponent: Component { private var isHighlightingEnabled: Bool = false init() { - self.button = HighlightTrackingButton() - self.highlightView = UIImageView() - self.highlightView.isUserInteractionEnabled = false - self.titleView = UIImageView() - self.titleView.isUserInteractionEnabled = false + self.highlightView = SimpleLayer() + self.titleView = SimpleLayer() super.init(frame: CGRect()) - self.button.addSubview(self.highlightView) - self.button.addSubview(self.titleView) + self.layer.addSublayer(self.highlightView) + self.layer.addSublayer(self.titleView) - self.addSubview(self.button) - - self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - self.button.highligthedChanged = { [weak self] highligthed in + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.highligthedChanged = { [weak self] highligthed in guard let strongSelf = self, let mediaPreviewView = strongSelf.mediaPreviewView else { return } if strongSelf.isHighlightingEnabled && highligthed { - mediaPreviewView.alpha = 0.8 + mediaPreviewView.opacity = 0.8 } else { let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(layer: mediaPreviewView.layer, alpha: 1.0) + transition.updateAlpha(layer: mediaPreviewView, alpha: 1.0) } } } @@ -489,9 +466,9 @@ private final class DayComponent: Component { let dayEnvironment = environment[DayEnvironment.self].value if component.media != nil { - self.highlightView.image = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: UIColor(white: 0.0, alpha: 0.2)) + self.highlightView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: UIColor(white: 0.0, alpha: 0.2)).cgImage } else { - self.highlightView.image = nil + self.highlightView.contents = nil } var animateMediaIn = false @@ -500,16 +477,15 @@ private final class DayComponent: Component { if let mediaPreviewView = self.mediaPreviewView { self.mediaPreviewView = nil - mediaPreviewView.removeFromSuperview() + mediaPreviewView.removeFromSuperlayer() } else { animateMediaIn = !isFirstTime } if let media = component.media { let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media, imageCache: dayEnvironment.directImageCache) - mediaPreviewView.isUserInteractionEnabled = false self.mediaPreviewView = mediaPreviewView - self.button.insertSubview(mediaPreviewView, belowSubview: self.highlightView) + self.layer.insertSublayer(mediaPreviewView, below: self.highlightView) } } @@ -552,24 +528,24 @@ private final class DayComponent: Component { switch component.selection { case .edge: - let selectionView: UIImageView + let selectionView: SimpleLayer if let current = self.selectionView { selectionView = current } else { - selectionView = UIImageView() + selectionView = SimpleLayer() self.selectionView = selectionView - self.button.insertSubview(selectionView, belowSubview: self.titleView) + self.layer.insertSublayer(selectionView, below: self.titleView) } selectionView.frame = contentFrame if self.mediaPreviewView != nil { - selectionView.image = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: diameter - 2.0 * 2.0, color: component.theme.list.itemCheckColors.fillColor) + selectionView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: diameter - 2.0 * 2.0, color: component.theme.list.itemCheckColors.fillColor).cgImage } else { - selectionView.image = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: component.theme.list.itemCheckColors.fillColor) + selectionView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: component.theme.list.itemCheckColors.fillColor).cgImage } case .middle, .none: if let selectionView = self.selectionView { self.selectionView = nil - selectionView.removeFromSuperview() + selectionView.removeFromSuperlayer() } } @@ -583,32 +559,31 @@ private final class DayComponent: Component { let titleImage = dayEnvironment.imageCache.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title) if animateMediaIn { - let previousTitleView = UIImageView(image: self.titleView.image) + let previousTitleView = SimpleLayer() + previousTitleView.contents = self.titleView.contents previousTitleView.frame = self.titleView.frame - self.titleView.superview?.insertSubview(previousTitleView, aboveSubview: self.titleView) - previousTitleView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousTitleView] _ in - previousTitleView?.removeFromSuperview() + self.titleView.superlayer?.insertSublayer(previousTitleView, above: self.titleView) + previousTitleView.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousTitleView] _ in + previousTitleView?.removeFromSuperlayer() }) - self.titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) + self.titleView.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) } - self.titleView.image = titleImage + self.titleView.contents = titleImage.cgImage let titleSize = titleImage.size - transition.setFrame(view: self.highlightView, frame: CGRect(origin: CGPoint(x: contentFrame.midX - contentFrame.width * contentScale / 2.0, y: contentFrame.midY - contentFrame.width * contentScale / 2.0), size: CGSize(width: contentFrame.width * contentScale, height: contentFrame.height * contentScale))) + self.highlightView.frame = CGRect(origin: CGPoint(x: contentFrame.midX - contentFrame.width * contentScale / 2.0, y: contentFrame.midY - contentFrame.width * contentScale / 2.0), size: CGSize(width: contentFrame.width * contentScale, height: contentFrame.height * contentScale)) self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) - self.button.frame = CGRect(origin: CGPoint(), size: availableSize) - if let mediaPreviewView = self.mediaPreviewView { mediaPreviewView.frame = contentFrame mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false) - mediaPreviewView.layer.sublayerTransform = CATransform3DMakeScale(contentScale, contentScale, 1.0) + mediaPreviewView.sublayerTransform = CATransform3DMakeScale(contentScale, contentScale, 1.0) if animateMediaIn { - mediaPreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.highlightView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + mediaPreviewView.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.highlightView.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } @@ -625,6 +600,219 @@ private final class DayComponent: Component { } } +private final class ManualMonthComponent: Component { + typealias EnvironmentType = DayEnvironment + + let context: AccountContext + let model: MonthModel + let foregroundColor: UIColor + let strings: PresentationStrings + let theme: PresentationTheme + let dayAction: (Int32) -> Void + let selectedDays: ClosedRange? + + init( + context: AccountContext, + model: MonthModel, + foregroundColor: UIColor, + strings: PresentationStrings, + theme: PresentationTheme, + dayAction: @escaping (Int32) -> Void, + selectedDays: ClosedRange? + ) { + self.context = context + self.model = model + self.foregroundColor = foregroundColor + self.strings = strings + self.theme = theme + self.dayAction = dayAction + self.selectedDays = selectedDays + } + + static func ==(lhs: ManualMonthComponent, rhs: ManualMonthComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.model != rhs.model { + return false + } + if lhs.foregroundColor != rhs.foregroundColor { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.selectedDays != rhs.selectedDays { + return false + } + return true + } + + final class View: UIView { + private let title: Text.View + private var weekdayTitles: [UIImageView] = [] + private var days: [Int: DayComponent.View] = [:] + + init() { + self.title = Text.View() + + super.init(frame: CGRect()) + + self.addSubview(self.title) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: ManualMonthComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + let sideInset: CGFloat = 14.0 + let titleWeekdaysSpacing: CGFloat = 18.0 + let weekdayDaySpacing: CGFloat = 14.0 + let weekdaySize: CGFloat = 46.0 + let weekdaySpacing: CGFloat = 6.0 + + let dayEnvironment = environment[DayEnvironment.self].value + + let usableWeekdayWidth = floor((availableSize.width - sideInset * 2.0 - weekdaySpacing * 6.0) / 7.0) + let weekdayWidth = floor((availableSize.width - sideInset * 2.0) / 7.0) + + let monthName = stringForMonth(strings: component.strings, month: Int32(component.model.index - 1), ofYear: Int32(component.model.year - 1900)) + + let titleSize = self.title.update( + component: Text( + text: monthName, + font: Font.semibold(17.0), + color: component.foregroundColor + ), + availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 0.0), size: titleSize) + self.title.frame = titleFrame + + for i in 0 ..< 7 { + let weekdayTitle: UIImageView + if self.weekdayTitles.count > i { + weekdayTitle = self.weekdayTitles[i] + } else { + weekdayTitle = UIImageView() + self.addSubview(weekdayTitle) + self.weekdayTitles.append(weekdayTitle) + } + let image = dayEnvironment.imageCache.text(fontSize: 10.0, isSemibold: false, color: component.foregroundColor, string: gridDayName(index: i, firstDayOfWeek: component.model.firstWeekday, strings: component.strings)) + weekdayTitle.image = image + } + + let baseWeekdayTitleY = titleFrame.maxY + titleWeekdaysSpacing + var maxWeekdayY = baseWeekdayTitleY + + for i in 0 ..< self.weekdayTitles.count { + guard let image = self.weekdayTitles[i].image else { + continue + } + let weekdaySize = image.size + let weekdayFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * weekdayWidth + floor((weekdayWidth - weekdaySize.width) / 2.0), y: baseWeekdayTitleY), size: weekdaySize) + maxWeekdayY = max(maxWeekdayY, weekdayFrame.maxY) + self.weekdayTitles[i].frame = weekdayFrame + } + + var daySizes: [Int: CGSize] = [:] + for index in 0 ..< component.model.numberOfDays { + let dayOfMonth = index + 1 + let isCurrent = component.model.currentYear == component.model.year && component.model.currentMonth == component.model.index && component.model.currentDayOfMonth == dayOfMonth + var isEnabled = true + if component.model.currentYear == component.model.year { + if component.model.currentMonth == component.model.index { + if dayOfMonth > component.model.currentDayOfMonth { + isEnabled = false + } + } else if component.model.index > component.model.currentMonth { + isEnabled = false + } + } else if component.model.year > component.model.currentYear { + isEnabled = false + } + + let dayTimestamp = Int32(component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(index) + let dayAction = component.dayAction + + let daySelection: DayComponent.DaySelection + if let selectedDays = component.selectedDays, selectedDays.contains(dayTimestamp) { + if selectedDays.lowerBound == dayTimestamp || selectedDays.upperBound == dayTimestamp { + daySelection = .edge + } else { + daySelection = .middle + } + } else { + daySelection = .none + } + + let day: DayComponent.View + if let current = self.days[index] { + day = current + } else { + day = DayComponent.View() + self.addSubview(day) + self.days[index] = day + } + + let daySize = day.update( + component: DayComponent( + title: "\(dayOfMonth)", + isCurrent: isCurrent, + isEnabled: isEnabled, + theme: component.theme, + context: component.context, + timestamp: dayTimestamp, + media: component.model.mediaByDay[index], + selection: daySelection, + isSelecting: component.selectedDays != nil, + action: { + dayAction(dayTimestamp) + } + ), + availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize), + environment: environment, + transition: .immediate + ) + daySizes[index] = daySize + } + + let baseDayY = maxWeekdayY + weekdayDaySpacing + var maxDayY = baseDayY + + for i in 0 ..< component.model.numberOfDays { + guard let dayView = self.days[i], let dayItemSize = daySizes[i] else { + continue + } + let gridIndex = gridDayOffset(firstDayOfWeek: component.model.firstWeekday, firstWeekdayOfMonth: component.model.firstDayWeekday) + i + let rowIndex = gridIndex % 7 + let lineIndex = gridIndex / 7 + + let gridX = sideInset + CGFloat(rowIndex) * weekdayWidth + let gridY = baseDayY + CGFloat(lineIndex) * (weekdaySize + weekdaySpacing) + let dayFrame = CGRect(origin: CGPoint(x: gridX + floor((weekdayWidth - dayItemSize.width) / 2.0), y: gridY + floor((weekdaySize - dayItemSize.height) / 2.0)), size: dayItemSize) + maxDayY = max(maxDayY, gridY + weekdaySize) + dayView.frame = dayFrame + } + + return CGSize(width: availableSize.width, height: maxDayY) + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} + private final class MonthComponent: CombinedComponent { typealias EnvironmentType = DayEnvironment @@ -1054,7 +1242,7 @@ public final class CalendarMessageScreen: ViewController { return false } - guard let dayView = result.superview as? DayComponent.View else { + guard let dayView = result as? DayComponent.View else { return false } diff --git a/submodules/ComponentFlow/Source/Components/Text.swift b/submodules/ComponentFlow/Source/Components/Text.swift index 153cfd551b..4c7b405ae6 100644 --- a/submodules/ComponentFlow/Source/Components/Text.swift +++ b/submodules/ComponentFlow/Source/Components/Text.swift @@ -30,7 +30,7 @@ public final class Text: Component { public final class View: UIView { private var measureState: MeasureState? - func update(component: Text, availableSize: CGSize) -> CGSize { + public func update(component: Text, availableSize: CGSize) -> CGSize { let attributedText = NSAttributedString(string: component.text, attributes: [ NSAttributedString.Key.font: component.font, NSAttributedString.Key.foregroundColor: component.color diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index a3a1775129..19f22159bb 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Accelerate import AsyncDisplayKit +import CoreMedia public let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { @@ -581,6 +582,30 @@ public class DrawingContext { return nil } } + + public func generatePixelBuffer() -> CVPixelBuffer? { + if self.scaledSize.width.isZero || self.scaledSize.height.isZero { + return nil + } + if self.hasGeneratedImage { + preconditionFailure() + } + + let ioSurfaceProperties = NSMutableDictionary() + let options = NSMutableDictionary() + options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) + + var pixelBuffer: CVPixelBuffer? + CVPixelBufferCreateWithBytes(nil, Int(self.scaledSize.width), Int(self.scaledSize.height), kCVPixelFormatType_32BGRA, self.bytes, self.bytesPerRow, { pointer, _ in + if let pointer = pointer { + Unmanaged.fromOpaque(pointer).release() + } + }, Unmanaged.passRetained(self.imageBuffer).toOpaque(), options as CFDictionary, &pixelBuffer) + + self.hasGeneratedImage = true + + return pixelBuffer + } public func colorAt(_ point: CGPoint) -> UIColor { let x = Int(point.x * self.scale) @@ -649,6 +674,76 @@ public class DrawingContext { } } +public extension UIImage { + var cvPixelBuffer: CVPixelBuffer? { + guard let cgImage = self.cgImage else { + return nil + } + let _ = cgImage + + var maybePixelBuffer: CVPixelBuffer? = nil + let ioSurfaceProperties = NSMutableDictionary() + let options = NSMutableDictionary() + options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) + + let _ = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width * self.scale), Int(size.height * self.scale), kCVPixelFormatType_32ARGB, options as CFDictionary, &maybePixelBuffer) + guard let pixelBuffer = maybePixelBuffer else { + return nil + } + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + } + + let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) + + let context = CGContext( + data: baseAddress, + width: Int(self.size.width * self.scale), + height: Int(self.size.height * self.scale), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue, + releaseCallback: nil, + releaseInfo: nil + )! + context.clear(CGRect(origin: .zero, size: CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale))) + context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale))) + + return pixelBuffer + } + + var cmSampleBuffer: CMSampleBuffer? { + guard let pixelBuffer = self.cvPixelBuffer else { + return nil + } + var newSampleBuffer: CMSampleBuffer? = nil + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var videoInfo: CMVideoFormatDescription? = nil + CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &videoInfo) + guard let videoInfo = videoInfo else { + return nil + } + CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo, sampleTiming: &timingInfo, sampleBufferOut: &newSampleBuffer) + + if let newSampleBuffer = newSampleBuffer { + let attachments = CMSampleBufferGetSampleAttachmentsArray(newSampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + } + + return newSampleBuffer + } +} + public enum ParsingError: Error { case Generic } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 6b93d5a465..0f9be9f0c5 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -150,6 +150,41 @@ private func cancelContextGestures(view: UIView) { } open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate { + public struct ScrollingIndicatorState { + public struct Item { + public var index: Int + public var offset: CGFloat + public var height: CGFloat + + public init( + index: Int, + offset: CGFloat, + height: CGFloat + ) { + self.index = index + self.offset = offset + self.height = height + } + } + + public var insets: UIEdgeInsets + public var topItem: Item + public var bottomItem: Item + public var itemCount: Int + + public init( + insets: UIEdgeInsets, + topItem: Item, + bottomItem: Item, + itemCount: Int + ) { + self.insets = insets + self.topItem = topItem + self.bottomItem = bottomItem + self.itemCount = itemCount + } + } + public final let scroller: ListViewScroller public private(set) final var visibleSize: CGSize = CGSize() public private(set) final var insets = UIEdgeInsets() @@ -214,14 +249,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } public final var snapToBottomInsetUntilFirstInteraction: Bool = false - public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? { - didSet { - - } - } - + public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?) -> Void)? public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)? + + public final var updateScrollingIndicator: ((ScrollingIndicatorState?, ContainedViewLayoutTransition) -> Void)? private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode? private var bottomItemOverscrollBackground: ASDisplayNode? @@ -3766,33 +3798,51 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture break } } - if let topIndexAndBoundary = topIndexAndBoundary, let bottomIndexAndBoundary = bottomIndexAndBoundary { + + var scrollingIndicatorStateValue: ScrollingIndicatorState? + if let topIndexAndBoundaryValue = topIndexAndBoundary, let bottomIndexAndBoundaryValue = bottomIndexAndBoundary { + let scrollingIndicatorState = ScrollingIndicatorState( + insets: self.insets, + topItem: ScrollingIndicatorState.Item( + index: topIndexAndBoundaryValue.0, + offset: topIndexAndBoundaryValue.1, + height: topIndexAndBoundaryValue.2 + ), + bottomItem: ScrollingIndicatorState.Item( + index: bottomIndexAndBoundaryValue.0, + offset: bottomIndexAndBoundaryValue.1, + height: bottomIndexAndBoundaryValue.2 + ), + itemCount: self.items.count + ) + scrollingIndicatorStateValue = scrollingIndicatorState + let averageRangeItemHeight: CGFloat = 44.0 - var upperItemsHeight = floor(averageRangeItemHeight * CGFloat(topIndexAndBoundary.0)) - var approximateContentHeight = CGFloat(self.items.count) * averageRangeItemHeight - if topIndexAndBoundary.0 >= 0 && self.items[topIndexAndBoundary.0].approximateHeight.isZero { + var upperItemsHeight = floor(averageRangeItemHeight * CGFloat(scrollingIndicatorState.topItem.index)) + var approximateContentHeight = CGFloat(scrollingIndicatorState.itemCount) * averageRangeItemHeight + if scrollingIndicatorState.topItem.index >= 0 && self.items[scrollingIndicatorState.topItem.index].approximateHeight.isZero { upperItemsHeight -= averageRangeItemHeight approximateContentHeight -= averageRangeItemHeight } var convertedTopBoundary: CGFloat - if topIndexAndBoundary.1 < self.insets.top { - convertedTopBoundary = (topIndexAndBoundary.1 - self.insets.top) * averageRangeItemHeight / topIndexAndBoundary.2 + if scrollingIndicatorState.topItem.offset < self.insets.top { + convertedTopBoundary = (scrollingIndicatorState.topItem.offset - scrollingIndicatorState.insets.top) * averageRangeItemHeight / scrollingIndicatorState.topItem.height } else { - convertedTopBoundary = topIndexAndBoundary.1 - self.insets.top + convertedTopBoundary = scrollingIndicatorState.topItem.offset - scrollingIndicatorState.insets.top } convertedTopBoundary -= upperItemsHeight let approximateOffset = -convertedTopBoundary var convertedBottomBoundary: CGFloat = 0.0 - if bottomIndexAndBoundary.1 > self.visibleSize.height - self.insets.bottom { - convertedBottomBoundary = ((self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1) * averageRangeItemHeight / bottomIndexAndBoundary.2 + if scrollingIndicatorState.bottomItem.offset > self.visibleSize.height - self.insets.bottom { + convertedBottomBoundary = ((self.visibleSize.height - scrollingIndicatorState.insets.bottom) - scrollingIndicatorState.bottomItem.offset) * averageRangeItemHeight / scrollingIndicatorState.bottomItem.height } else { - convertedBottomBoundary = (self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1 + convertedBottomBoundary = (self.visibleSize.height - scrollingIndicatorState.insets.bottom) - scrollingIndicatorState.bottomItem.offset } - convertedBottomBoundary += CGFloat(bottomIndexAndBoundary.0 + 1) * averageRangeItemHeight + convertedBottomBoundary += CGFloat(scrollingIndicatorState.bottomItem.index + 1) * averageRangeItemHeight let approximateVisibleHeight = max(0.0, convertedBottomBoundary - approximateOffset) @@ -3801,8 +3851,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let indicatorSideInset: CGFloat = 3.0 var indicatorTopInset: CGFloat = 3.0 if self.verticalScrollIndicatorFollowsOverscroll { - if topIndexAndBoundary.0 == 0 { - indicatorTopInset = max(topIndexAndBoundary.1 + 3.0 - self.insets.top, 3.0) + if scrollingIndicatorState.topItem.index == 0 { + indicatorTopInset = max(scrollingIndicatorState.topItem.offset + 3.0 - self.insets.top, 3.0) } } let indicatorBottomInset: CGFloat = 3.0 @@ -3814,7 +3864,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if approximateContentHeight <= 0 { indicatorHeight = 0.0 } else { - indicatorHeight = max(minIndicatorContentHeight, floor(visibleHeightWithoutIndicatorInsets * (self.visibleSize.height - self.insets.top - self.insets.bottom) / approximateContentHeight)) + indicatorHeight = max(minIndicatorContentHeight, floor(visibleHeightWithoutIndicatorInsets * (self.visibleSize.height - scrollingIndicatorState.insets.top - scrollingIndicatorState.insets.bottom) / approximateContentHeight)) } let upperBound = self.scrollIndicatorInsets.top + indicatorTopInset @@ -3852,6 +3902,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { verticalScrollIndicator.isHidden = true } + + self.updateScrollingIndicator?(scrollingIndicatorStateValue, transition) } } @@ -4638,7 +4690,3 @@ private func findAccessibilityFocus(_ node: ASDisplayNode) -> Bool { } return false } - -public func randomfqweeqwf() { - print("t") -} diff --git a/submodules/Display/Source/TransformImageNode.swift b/submodules/Display/Source/TransformImageNode.swift index 1cb1926a3f..93285773a1 100644 --- a/submodules/Display/Source/TransformImageNode.swift +++ b/submodules/Display/Source/TransformImageNode.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import SwiftSignalKit +import AVFoundation public struct TransformImageNodeContentAnimations: OptionSet { public var rawValue: Int32 @@ -21,10 +22,60 @@ open class TransformImageNode: ASDisplayNode { private var currentTransform: ((TransformImageArguments) -> DrawingContext?)? private var currentArguments: TransformImageArguments? + private var image: UIImage? private var argumentsPromise = ValuePromise(ignoreRepeated: true) private var overlayColor: UIColor? private var overlayNode: ASDisplayNode? + + private var captureProtectedContentLayer: CaptureProtectedContentLayer? + + public var captureProtected: Bool = false { + didSet { + if self.captureProtected != oldValue { + if self.captureProtected { + if self.captureProtectedContentLayer == nil { + let captureProtectedContentLayer = CaptureProtectedContentLayer() + self.captureProtectedContentLayer = captureProtectedContentLayer + if #available(iOS 13.0, *) { + captureProtectedContentLayer.preventsCapture = true + captureProtectedContentLayer.preventsDisplaySleepDuringVideoPlayback = false + } + captureProtectedContentLayer.frame = self.bounds + self.layer.addSublayer(captureProtectedContentLayer) + if let image = self.image { + if let cmSampleBuffer = image.cmSampleBuffer { + captureProtectedContentLayer.enqueue(cmSampleBuffer) + } + } + self.contents = nil + } + } else if let captureProtectedContentLayer = self.captureProtectedContentLayer { + self.captureProtectedContentLayer = nil + captureProtectedContentLayer.removeFromSuperlayer() + } + } + } + } + + open override var bounds: CGRect { + didSet { + if let captureProtectedContentLayer = self.captureProtectedContentLayer, super.bounds.size != oldValue.size { + captureProtectedContentLayer.frame = super.bounds + } + } + } + + open override var frame: CGRect { + didSet { + if let overlayNode = self.overlayNode { + overlayNode.frame = self.bounds + } + if let captureProtectedContentLayer = self.captureProtectedContentLayer, super.bounds.size != oldValue.size { + captureProtectedContentLayer.frame = super.bounds + } + } + } deinit { self.disposable.dispose() @@ -38,19 +89,12 @@ open class TransformImageNode: ASDisplayNode { } } - override open var frame: CGRect { - didSet { - if let overlayNode = self.overlayNode { - overlayNode.frame = self.bounds - } - } - } - public func reset() { self.disposable.set(nil) self.currentArguments = nil self.currentTransform = nil self.contents = nil + self.image = nil } public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true) { @@ -85,21 +129,31 @@ open class TransformImageNode: ASDisplayNode { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } else if strongSelf.contentAnimations.contains(.subsequentUpdates) { - let tempLayer = CALayer() - tempLayer.frame = strongSelf.bounds - tempLayer.contentsGravity = strongSelf.layer.contentsGravity - tempLayer.contents = strongSelf.contents - strongSelf.layer.addSublayer(tempLayer) - tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in - tempLayer?.removeFromSuperlayer() - }) + if let _ = strongSelf.captureProtectedContentLayer { + } else { + let tempLayer = CALayer() + tempLayer.frame = strongSelf.bounds + tempLayer.contentsGravity = strongSelf.layer.contentsGravity + tempLayer.contents = strongSelf.contents + strongSelf.layer.addSublayer(tempLayer) + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) + } } var imageUpdate: UIImage? if let (transform, arguments, image) = next { strongSelf.currentTransform = transform strongSelf.currentArguments = arguments - strongSelf.contents = image?.cgImage + if let captureProtectedContentLayer = strongSelf.captureProtectedContentLayer { + if let cmSampleBuffer = image?.cmSampleBuffer { + captureProtectedContentLayer.enqueue(cmSampleBuffer) + } + } else { + strongSelf.contents = image?.cgImage + } + strongSelf.image = image imageUpdate = image } if let _ = strongSelf.overlayColor { @@ -135,7 +189,14 @@ open class TransformImageNode: ASDisplayNode { return } if let image = updatedImage { - strongSelf.contents = image.cgImage + if let captureProtectedContentLayer = strongSelf.captureProtectedContentLayer { + if let cmSampleBuffer = image.cmSampleBuffer { + captureProtectedContentLayer.enqueue(cmSampleBuffer) + } + } else { + strongSelf.contents = image.cgImage + } + strongSelf.image = image strongSelf.currentArguments = arguments if let _ = strongSelf.overlayColor { strongSelf.applyOverlayColor(animated: false) @@ -207,6 +268,19 @@ open class TransformImageNode: ASDisplayNode { } } +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +private class CaptureProtectedContentLayer: AVSampleBufferDisplayLayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } +} + open class TransformImageView: UIView { public var imageUpdated: ((UIImage?) -> Void)? public var contentAnimations: TransformImageNodeContentAnimations = [] @@ -215,10 +289,55 @@ open class TransformImageView: UIView { private var currentTransform: ((TransformImageArguments) -> DrawingContext?)? private var currentArguments: TransformImageArguments? private var argumentsPromise = ValuePromise(ignoreRepeated: true) + private var image: UIImage? + + private var captureProtectedContentLayer: CaptureProtectedContentLayer? private var overlayColor: UIColor? private var overlayView: UIView? + open override var bounds: CGRect { + didSet { + if let captureProtectedContentLayer = self.captureProtectedContentLayer, super.bounds.size != oldValue.size { + captureProtectedContentLayer.frame = super.bounds + } + } + } + + open override var frame: CGRect { + didSet { + if let overlayView = self.overlayView { + overlayView.frame = self.bounds + } + if let captureProtectedContentLayer = self.captureProtectedContentLayer, super.bounds.size != oldValue.size { + captureProtectedContentLayer.frame = super.bounds + } + } + } + + public var captureProtected: Bool = false { + didSet { + if self.captureProtected != oldValue { + if self.captureProtected { + if self.captureProtectedContentLayer == nil { + let captureProtectedContentLayer = CaptureProtectedContentLayer() + captureProtectedContentLayer.frame = self.bounds + self.layer.addSublayer(captureProtectedContentLayer) + if let image = self.image { + if let cmSampleBuffer = image.cmSampleBuffer { + captureProtectedContentLayer.enqueue(cmSampleBuffer) + } + } + self.layer.contents = nil + } + } else if let captureProtectedContentLayer = self.captureProtectedContentLayer { + self.captureProtectedContentLayer = nil + captureProtectedContentLayer.removeFromSuperlayer() + } + } + } + } + override public init(frame: CGRect) { super.init(frame: frame) @@ -235,19 +354,13 @@ open class TransformImageView: UIView { self.disposable.dispose() } - override open var frame: CGRect { - didSet { - if let overlayView = self.overlayView { - overlayView.frame = self.bounds - } - } - } - public func reset() { self.disposable.set(nil) self.currentArguments = nil self.currentTransform = nil self.layer.contents = nil + self.image = nil + self.captureProtectedContentLayer?.flushAndRemoveImage() } public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true) { @@ -277,26 +390,36 @@ open class TransformImageView: UIView { self.disposable.set((result |> deliverOnMainQueue).start(next: { [weak self] next in let apply: () -> Void = { if let strongSelf = self { - if strongSelf.layer.contents == nil { + if strongSelf.image == nil { if strongSelf.contentAnimations.contains(.firstUpdate) && !attemptSynchronously { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } else if strongSelf.contentAnimations.contains(.subsequentUpdates) { - let tempLayer = CALayer() - tempLayer.frame = strongSelf.bounds - tempLayer.contentsGravity = strongSelf.layer.contentsGravity - tempLayer.contents = strongSelf.layer.contents - strongSelf.layer.addSublayer(tempLayer) - tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in - tempLayer?.removeFromSuperlayer() - }) + if let _ = strongSelf.captureProtectedContentLayer { + } else { + let tempLayer = CALayer() + tempLayer.frame = strongSelf.bounds + tempLayer.contentsGravity = strongSelf.layer.contentsGravity + tempLayer.contents = strongSelf.layer.contents + strongSelf.layer.addSublayer(tempLayer) + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) + } } var imageUpdate: UIImage? if let (transform, arguments, image) = next { strongSelf.currentTransform = transform strongSelf.currentArguments = arguments - strongSelf.layer.contents = image?.cgImage + if let captureProtectedContentLayer = strongSelf.captureProtectedContentLayer { + if let cmSampleBuffer = image?.cmSampleBuffer { + captureProtectedContentLayer.enqueue(cmSampleBuffer) + } + } else { + strongSelf.layer.contents = image?.cgImage + } + strongSelf.image = image imageUpdate = image } if let _ = strongSelf.overlayColor { @@ -369,13 +492,13 @@ open class TransformImageView: UIView { private func applyOverlayColor(animated: Bool) { if let overlayColor = self.overlayColor { - if let contents = self.layer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + if let image = self.image { if let overlayView = self.overlayView { - (overlayView as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + (overlayView as! UIImageView).image = UIImage(cgImage: image.cgImage!).withRenderingMode(.alwaysTemplate) overlayView.tintColor = overlayColor } else { let overlayView = UIImageView() - overlayView.image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + overlayView.image = UIImage(cgImage: image.cgImage!).withRenderingMode(.alwaysTemplate) overlayView.tintColor = overlayColor overlayView.frame = self.bounds self.addSubview(overlayView) diff --git a/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift new file mode 100644 index 0000000000..278c3387cd --- /dev/null +++ b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift @@ -0,0 +1,393 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import TelegramPresentationData + +public final class SparseDiscreteScrollingArea: ASDisplayNode { + private final class DragGesture: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> Bool + private let began: () -> Void + private let ended: () -> Void + private let moved: (CGFloat) -> Void + + private var initialLocation: CGPoint? + + public init( + shouldBegin: @escaping (CGPoint) -> Bool, + began: @escaping () -> Void, + ended: @escaping () -> Void, + moved: @escaping (CGFloat) -> Void + ) { + self.shouldBegin = shouldBegin + self.began = began + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + } + + deinit { + } + + override public func reset() { + super.reset() + + self.initialLocation = nil + self.initialLocation = nil + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + if self.shouldBegin(location) { + self.initialLocation = location + self.state = .began + self.began() + } else { + self.state = .failed + } + } else { + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = location.y - initialLocation.y + self.moved(offset) + } + } + } + + private let dateIndicator: ComponentHostView + private let lineIndicator: UIImageView + + private var containerSize: CGSize? + private var indicatorPosition: CGFloat? + private var scrollIndicatorHeight: CGFloat? + + private var dragGesture: DragGesture? + public private(set) var isDragging: Bool = false + + private var activityTimer: SwiftSignalKit.Timer? + + public var openCurrentDate: (() -> Void)? + + private var offsetBarTimer: SwiftSignalKit.Timer? + private let hapticFeedback = HapticFeedback() + + private var theme: PresentationTheme? + + override public init() { + self.dateIndicator = ComponentHostView() + self.lineIndicator = UIImageView() + + self.dateIndicator.alpha = 0.0 + self.lineIndicator.alpha = 0.0 + + super.init() + + self.dateIndicator.isUserInteractionEnabled = false + self.lineIndicator.isUserInteractionEnabled = false + + self.view.addSubview(self.dateIndicator) + self.view.addSubview(self.lineIndicator) + + let dragGesture = DragGesture( + shouldBegin: { [weak self] point in + guard let _ = self else { + return false + } + return true + }, + began: { [weak self] in + guard let strongSelf = self else { + return + } + + let offsetBarTimer = SwiftSignalKit.Timer(timeout: 0.2, repeat: false, completion: { + guard let strongSelf = self else { + return + } + strongSelf.performOffsetBarTimerEvent() + }, queue: .mainQueue()) + strongSelf.offsetBarTimer?.invalidate() + strongSelf.offsetBarTimer = offsetBarTimer + offsetBarTimer.start() + + strongSelf.isDragging = true + + /*if let scrollView = strongSelf.beginScrolling?() { + strongSelf.draggingScrollView = scrollView + strongSelf.scrollingInitialOffset = scrollView.contentOffset.y + strongSelf.setContentOffset?(scrollView.contentOffset) + }*/ + + strongSelf.updateActivityTimer(isScrolling: false) + }, + ended: { [weak self] in + guard let strongSelf = self else { + return + } + if strongSelf.offsetBarTimer != nil { + strongSelf.offsetBarTimer?.invalidate() + strongSelf.offsetBarTimer = nil + + strongSelf.openCurrentDate?() + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + transition.updateSublayerTransformOffset(layer: strongSelf.dateIndicator.layer, offset: CGPoint(x: 0.0, y: 0.0)) + + strongSelf.isDragging = false + + //strongSelf.updateLineIndicator(transition: transition) + + strongSelf.updateActivityTimer(isScrolling: false) + }, + moved: { [weak self] relativeOffset in + guard let strongSelf = self else { + return + } + + let _ = relativeOffset + + if strongSelf.offsetBarTimer != nil { + strongSelf.offsetBarTimer?.invalidate() + strongSelf.offsetBarTimer = nil + strongSelf.performOffsetBarTimerEvent() + } + } + ) + self.dragGesture = dragGesture + + self.view.addGestureRecognizer(dragGesture) + } + + private func performOffsetBarTimerEvent() { + self.hapticFeedback.impact() + self.offsetBarTimer = nil + + /*let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut) + transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint(x: -80.0, y: 0.0)) + self.updateLineIndicator(transition: transition)*/ + } + + func feedbackTap() { + self.hapticFeedback.tap() + } + + public func update( + containerSize: CGSize, + containerInsets: UIEdgeInsets, + scrollingState: ListView.ScrollingIndicatorState?, + isScrolling: Bool, + theme: PresentationTheme, + transition: ContainedViewLayoutTransition + ) { + self.containerSize = containerSize + if self.theme !== theme { + self.theme = theme + + /*var backgroundColors: [UInt32] = [] + switch chatPresentationInterfaceState.chatWallpaper { + case let .file(file): + if file.isPattern { + backgroundColors = file.settings.colors + } + case let .gradient(gradient): + backgroundColors = gradient.colors + case let .color(color): + backgroundColors = [color] + default: + break + }*/ + let lineColor: UIColor + if theme.overallDarkAppearance { + lineColor = UIColor(white: 0.0, alpha: 0.3) + } else { + lineColor = UIColor(white: 0.0, alpha: 0.3) + } + self.lineIndicator.image = generateStretchableFilledCircleImage(diameter: 3.0, color: lineColor, strokeColor: nil, strokeWidth: nil, backgroundColor: nil) + } + + if self.dateIndicator.alpha.isZero { + let transition: ContainedViewLayoutTransition = .immediate + transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint()) + } + + if isScrolling { + self.updateActivityTimer(isScrolling: true) + } + + let indicatorSize = self.dateIndicator.update( + transition: .immediate, + component: AnyComponent(SparseItemGridScrollingIndicatorComponent( + backgroundColor: theme.list.itemBlocksBackgroundColor, + shadowColor: .black, + foregroundColor: theme.list.itemPrimaryTextColor, + dateString: "Date" + )), + environment: {}, + containerSize: containerSize + ) + let _ = indicatorSize + + self.dateIndicator.isHidden = true + + + if let scrollingIndicatorState = scrollingState { + let averageRangeItemHeight: CGFloat = 44.0 + + let upperItemsHeight = floor(averageRangeItemHeight * CGFloat(scrollingIndicatorState.topItem.index)) + let approximateContentHeight = CGFloat(scrollingIndicatorState.itemCount) * averageRangeItemHeight + + var convertedTopBoundary: CGFloat + if scrollingIndicatorState.topItem.offset < scrollingIndicatorState.insets.top { + convertedTopBoundary = (scrollingIndicatorState.topItem.offset - scrollingIndicatorState.insets.top) * averageRangeItemHeight / scrollingIndicatorState.topItem.height + } else { + convertedTopBoundary = scrollingIndicatorState.topItem.offset - scrollingIndicatorState.insets.top + } + convertedTopBoundary -= upperItemsHeight + + let approximateOffset = -convertedTopBoundary + + var convertedBottomBoundary: CGFloat = 0.0 + if scrollingIndicatorState.bottomItem.offset > containerSize.height - scrollingIndicatorState.insets.bottom { + convertedBottomBoundary = ((containerSize.height - scrollingIndicatorState.insets.bottom) - scrollingIndicatorState.bottomItem.offset) * averageRangeItemHeight / scrollingIndicatorState.bottomItem.height + } else { + convertedBottomBoundary = (containerSize.height - scrollingIndicatorState.insets.bottom) - scrollingIndicatorState.bottomItem.offset + } + convertedBottomBoundary += CGFloat(scrollingIndicatorState.bottomItem.index + 1) * averageRangeItemHeight + + let approximateVisibleHeight = max(0.0, convertedBottomBoundary - approximateOffset) + + let approximateScrollingProgress = approximateOffset / (approximateContentHeight - approximateVisibleHeight) + + let indicatorSideInset: CGFloat = 3.0 + let indicatorTopInset: CGFloat = 3.0 + /*if self.verticalScrollIndicatorFollowsOverscroll { + if scrollingIndicatorState.topItem.index == 0 { + indicatorTopInset = max(scrollingIndicatorState.topItem.offset + 3.0 - self.insets.top, 3.0) + } + }*/ + let indicatorBottomInset: CGFloat = 3.0 + let minIndicatorContentHeight: CGFloat = 12.0 + let minIndicatorHeight: CGFloat = 6.0 + + let visibleHeightWithoutIndicatorInsets = containerSize.height - containerInsets.top - containerInsets.bottom - indicatorTopInset - indicatorBottomInset + let indicatorHeight: CGFloat + if approximateContentHeight <= 0 { + indicatorHeight = 0.0 + } else { + indicatorHeight = max(minIndicatorContentHeight, floor(visibleHeightWithoutIndicatorInsets * (containerSize.height - scrollingIndicatorState.insets.top - scrollingIndicatorState.insets.bottom) / approximateContentHeight)) + } + + let upperBound = containerInsets.top + indicatorTopInset + let lowerBound = containerSize.height - containerInsets.bottom - indicatorTopInset - indicatorBottomInset - indicatorHeight + + let indicatorOffset = ceilToScreenPixels(upperBound * (1.0 - approximateScrollingProgress) + lowerBound * approximateScrollingProgress) + + //var indicatorFrame = CGRect(origin: CGPoint(x: self.rotated ? indicatorSideInset : (self.visibleSize.width - 3.0 - indicatorSideInset), y: indicatorOffset), size: CGSize(width: 3.0, height: indicatorHeight)) + + var indicatorFrame = CGRect(origin: CGPoint(x: containerSize.width - 3.0 - indicatorSideInset, y: indicatorOffset), size: CGSize(width: 3.0, height: indicatorHeight)) + + if indicatorFrame.minY < containerInsets.top + indicatorTopInset { + indicatorFrame.size.height -= containerInsets.top + indicatorTopInset - indicatorFrame.minY + indicatorFrame.origin.y = containerInsets.top + indicatorTopInset + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) + } + if indicatorFrame.maxY > containerSize.height - (containerInsets.bottom + indicatorTopInset + indicatorBottomInset) { + indicatorFrame.size.height -= indicatorFrame.maxY - (containerSize.height - (containerInsets.bottom + indicatorTopInset)) + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) + indicatorFrame.origin.y = containerSize.height - (containerInsets.bottom + indicatorBottomInset) - indicatorFrame.height + } + + if indicatorFrame.origin.y.isNaN { + indicatorFrame.origin.y = indicatorTopInset + } + + if indicatorHeight >= visibleHeightWithoutIndicatorInsets { + self.lineIndicator.isHidden = true + self.lineIndicator.frame = indicatorFrame + } else { + if self.lineIndicator.isHidden { + self.lineIndicator.isHidden = false + self.lineIndicator.frame = indicatorFrame + } else { + self.lineIndicator.frame = indicatorFrame + } + } + } else { + self.lineIndicator.isHidden = true + } + } + + private func updateActivityTimer(isScrolling: Bool) { + self.activityTimer?.invalidate() + + if self.isDragging { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) + transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) + } else { + self.activityTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0) + transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0) + }, queue: .mainQueue()) + self.activityTimer?.start() + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.lineIndicator.alpha <= 0.01 { + return nil + } + if self.lineIndicator.frame.insetBy(dx: -4.0, dy: -2.0).contains(point) { + return super.hitTest(point, with: event) + } + + return nil + } +} diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 20eadc11bb..c0f4e37302 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -555,7 +555,7 @@ private final class ShadowRoundedRectangle: Component { } } -private final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { +final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { let backgroundColor: UIColor let shadowColor: UIColor let foregroundColor: UIColor diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index ae0191afa3..bc99ee142a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -709,6 +709,111 @@ public final class SparseMessageList { } } +public final class SparseMessageScrollingContext { + public struct State: Equatable { + public var totalCount: Int + public var minTimestamp: Int32 + } + + private final class Impl { + private let queue: Queue + private let account: Account + private let peerId: PeerId + + let statePromise = Promise() + + private let disposable = MetaDisposable() + + init(queue: Queue, account: Account, peerId: PeerId) { + self.queue = queue + self.account = account + self.peerId = peerId + + self.reload() + } + + deinit { + self.disposable.dispose() + } + + private func reload() { + let account = self.account + let peerId = self.peerId + + let signal: Signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(nil) + } + return account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: 1, offsetDate: 0, addOffset: -1, limit: 1, maxId: 0, minId: 0, hash: 0)) + |> map { result -> State? in + let messages: [Api.Message] + let totalCount: Int + + switch result { + case let .messages(apiMessages, _, _): + messages = apiMessages + totalCount = messages.count + case let .messagesSlice(_, count, _, _, apiMessages, _, _): + messages = apiMessages + totalCount = Int(count) + case let .channelMessages(_, _, count, _, apiMessages, _, _): + messages = apiMessages + totalCount = Int(count) + case .messagesNotModified: + messages = [] + totalCount = 0 + } + + if let apiMessage = messages.first, let message = StoreMessage(apiMessage: apiMessage) { + return State(totalCount: totalCount, minTimestamp: message.timestamp) + } else { + return State(totalCount: 0, minTimestamp: 0) + } + } + |> `catch` { _ -> Signal in + return .single(nil) + } + } + + self.disposable.set((signal |> deliverOn(self.queue)).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + if let state = state { + strongSelf.statePromise.set(.single(state)) + } + })) + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.statePromise.get().start(next: subscriber.putNext)) + } + + return disposable + } + } + + init(account: Account, peerId: PeerId) { + let queue = Queue() + self.queue = queue + + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId) + }) + } +} + public final class SparseMessageCalendar { private final class Impl { struct InternalState { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index c60a74cc9c..ae3f5417e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -251,6 +251,10 @@ public extension TelegramEngine { return SparseMessageCalendar(account: self.account, peerId: peerId, messageTag: tag) } + public func sparseMessageScrollingContext(peerId: EnginePeer.Id) -> SparseMessageScrollingContext { + return SparseMessageScrollingContext(account: self.account, peerId: peerId) + } + public func refreshMessageTagStats(peerId: EnginePeer.Id, tags: [EngineMessage.Tags]) -> Signal { let account = self.account return self.account.postbox.transaction { transaction -> Api.InputPeer? in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 20c49b6e8a..2db64d996f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -16,6 +16,7 @@ import FastBlur import ConfettiEffect import WallpaperBackgroundNode import GridMessageSelectionNode +import SparseItemGrid final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -81,6 +82,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let backgroundNode: WallpaperBackgroundNode let historyNode: ChatHistoryListNode + let historyScrollingArea: SparseDiscreteScrollingArea var blurredHistoryNode: ASImageNode? let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode @@ -323,8 +325,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return getMessageTransitionNode?() }) self.historyNode.rotated = true + + self.historyScrollingArea = SparseDiscreteScrollingArea() + self.historyNodeContainer = ASDisplayNode() self.historyNodeContainer.addSubnode(self.historyNode) + self.historyNodeContainer.addSubnode(self.historyScrollingArea) var getContentAreaInScreenSpaceImpl: (() -> CGRect)? var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? @@ -438,7 +444,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } }) - var backgroundColors: [UInt32] = [] + /*var backgroundColors: [UInt32] = [] switch chatPresentationInterfaceState.chatWallpaper { case let .file(file): if file.isPattern { @@ -461,7 +467,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) } - self.historyNode.enableExtractedBackgrounds = true + self.historyNode.enableExtractedBackgrounds = true*/ + self.historyNode.verticalScrollIndicatorColor = .clear self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNodeContainer) @@ -522,6 +529,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textInputPanelNode?.updateActivity = { [weak self] in self?.updateTypingActivity(true) } + + self.historyNode.updateScrollingIndicator = { [weak self] scrollingState, transition in + guard let strongSelf = self else { + return + } + guard let (_, _) = strongSelf.validLayout else { + return + } + strongSelf.historyScrollingArea.update( + containerSize: strongSelf.historyNode.bounds.size, + containerInsets: UIEdgeInsets(top: strongSelf.historyNode.scrollIndicatorInsets.bottom, left: 0.0, bottom: strongSelf.historyNode.scrollIndicatorInsets.top, right: 0.0), + scrollingState: scrollingState, + isScrolling: true, + theme: strongSelf.chatPresentationInterfaceState.theme, + transition: transition + ) + } } deinit { @@ -1041,6 +1065,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let blurredHistoryNode = self.blurredHistoryNode { transition.updateFrame(node: blurredHistoryNode, frame: contentBounds) } + + transition.updateFrame(node: self.historyScrollingArea, frame: contentBounds) transition.updateFrame(node: self.loadingNode, frame: contentBounds) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 414a9b0879..fbcfe05213 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -562,6 +562,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let adMessagesContext: AdMessagesHistoryContext? private var preloadAdPeerId: PeerId? private let preloadAdPeerDisposable = MetaDisposable() + + private let sparseScrollingContext: SparseMessageScrollingContext? private let clientId: Atomic @@ -588,8 +590,19 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.prefetchManager = InChatPrefetchManager(context: context) + var displayAdPeer: PeerId? + var sparseScrollPeerId: PeerId? + switch subject { + case .none, .message: + if case let .peer(peerId) = chatLocation { + displayAdPeer = peerId + sparseScrollPeerId = peerId + } + default: + break + } var adMessages: Signal<[Message], NoError> - if case .bubbles = mode, case let .peer(peerId) = chatLocation, case .none = subject { + if case .bubbles = mode, let peerId = displayAdPeer { let adMessagesContext = context.engine.messages.adMessages(peerId: peerId) self.adMessagesContext = adMessagesContext adMessages = adMessagesContext.state @@ -597,6 +610,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.adMessagesContext = nil adMessages = .single([]) } + + if case .bubbles = mode, let peerId = sparseScrollPeerId { + self.sparseScrollingContext = context.engine.messages.sparseMessageScrollingContext(peerId: peerId) + } else { + self.sparseScrollingContext = nil + } let clientId = Atomic(value: nextClientId) self.clientId = clientId diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index fd984494ea..07d3fdc11a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -969,6 +969,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads) var imageDimensions: CGSize? diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 1446db7a84..ad9857853d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -321,7 +321,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { break } } - } else if let _ = item.message.adAttribute { + } else if let adAttribute = item.message.adAttribute { title = nil subtitle = nil text = item.message.text @@ -342,9 +342,17 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let author = item.message.author as? TelegramUser, author.botInfo != nil { actionTitle = item.presentationData.strings.Conversation_ViewBot } else if let author = item.message.author as? TelegramChannel, case .group = author.info { - actionTitle = item.presentationData.strings.Conversation_ViewGroup + if adAttribute.messageId != nil { + actionTitle = item.presentationData.strings.Conversation_ViewPost + } else { + actionTitle = item.presentationData.strings.Conversation_ViewGroup + } } else { - actionTitle = item.presentationData.strings.Conversation_ViewChannel + if adAttribute.messageId != nil { + actionTitle = item.presentationData.strings.Conversation_ViewMessage + } else { + actionTitle = item.presentationData.strings.Conversation_ViewChannel + } } displayLine = false }