diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index 8c8fab3ba8..0e91535a17 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -129,20 +129,4 @@ def generate(build_environment: BuildEnvironment, disable_extensions, disable_pr xcodeproj_path = '{project}/{target}.xcodeproj'.format(project=project_path, target=app_target) - bazel_build_settings_path = '{}/.tulsi/Scripts/bazel_build_settings.py'.format(xcodeproj_path) - - with open(bazel_build_settings_path, 'rb') as bazel_build_settings: - bazel_build_settings_contents = bazel_build_settings.read().decode('utf-8') - bazel_build_settings_contents = bazel_build_settings_contents.replace( - 'BUILD_SETTINGS = BazelBuildSettings(', - 'import os\nBUILD_SETTINGS = BazelBuildSettings(' - ) - bazel_build_settings_contents = bazel_build_settings_contents.replace( - '\'--cpu=ios_arm64\'', - '\'--cpu=ios_arm64\'.replace(\'ios_arm64\', \'ios_sim_arm64\' if os.environ.get(\'EFFECTIVE_PLATFORM_NAME\') ' - '== \'-iphonesimulator\' else \'ios_arm64\')' - ) - with open(bazel_build_settings_path, 'wb') as bazel_build_settings: - bazel_build_settings.write(bazel_build_settings_contents.encode('utf-8')) - call_executable(['open', xcodeproj_path]) diff --git a/build-system/bazel-rules/rules_swift b/build-system/bazel-rules/rules_swift index 8c8f4661db..03c89782e9 160000 --- a/build-system/bazel-rules/rules_swift +++ b/build-system/bazel-rules/rules_swift @@ -1 +1 @@ -Subproject commit 8c8f4661dba2bbe8578ae42b8ab7001d27357575 +Subproject commit 03c89782e9a15d467c7e036ee36f9adb6bdda910 diff --git a/build-system/tulsi b/build-system/tulsi index 01d37ab862..ec7dd9ddf4 160000 --- a/build-system/tulsi +++ b/build-system/tulsi @@ -1 +1 @@ -Subproject commit 01d37ab862350cb33cbae25cf6622bf534df264f +Subproject commit ec7dd9ddf4b73dedb02df827b7ab3b2cbb1f2ac0 diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index d0f000eaf8..4ead2246a4 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -10,12 +10,12 @@ import TelegramPresentationData import ComponentFlow import PhotoResources -private final class MediaPreviewNode: ASDisplayNode { +private final class MediaPreviewView: UIView { private let context: AccountContext private let message: EngineMessage private let media: EngineMedia - private let imageNode: TransformImageNode + private let imageView: TransformImageView private var requestedImage: Bool = false private var disposable: Disposable? @@ -25,11 +25,15 @@ private final class MediaPreviewNode: ASDisplayNode { self.message = message self.media = media - self.imageNode = TransformImageNode() + self.imageView = TransformImageView() - super.init() + super.init(frame: CGRect()) - self.addSubnode(self.imageNode) + self.addSubview(self.imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } deinit { @@ -44,7 +48,7 @@ private final class MediaPreviewNode: ASDisplayNode { 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.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) + self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) } } } else if case let .file(file) = self.media { @@ -53,13 +57,13 @@ private final class MediaPreviewNode: ASDisplayNode { 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.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) + self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) } } } - let makeLayout = self.imageNode.asyncLayout() - self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + 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() } @@ -152,6 +156,13 @@ private final class ImageCache: Equatable { var color: UInt32 } + private struct Text: Hashable { + var fontSize: CGFloat + var isSemibold: Bool + var color: UInt32 + var string: String + } + private var items: [AnyHashable: UIImage] = [:] func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage { @@ -169,6 +180,31 @@ private final class ImageCache: Equatable { self.items[key] = image return image } + + func text(fontSize: CGFloat, isSemibold: Bool, color: UIColor, string: String) -> UIImage { + let key = AnyHashable(Text(fontSize: fontSize, isSemibold: isSemibold, color: color.argb, string: string)) + if let image = self.items[key] { + return image + } + + let font: UIFont + if isSemibold { + font = Font.semibold(fontSize) + } else { + font = Font.regular(fontSize) + } + let attributedString = NSAttributedString(string: string, font: font, textColor: color) + let rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil) + let image = generateImage(CGSize(width: ceil(rect.width), height: ceil(rect.height)), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + UIGraphicsPushContext(context) + attributedString.draw(in: rect) + UIGraphicsPopContext() + })! + self.items[key] = image + return image + } } private final class DayComponent: Component { @@ -223,28 +259,28 @@ private final class DayComponent: Component { } final class View: UIView { - private let buttonNode: HighlightableButtonNode + private let button: HighlightableButton - private let highlightNode: ASImageNode - private let titleNode: ImmediateTextNode - private var mediaPreviewNode: MediaPreviewNode? + private let highlightView: UIImageView + private let titleView: UIImageView + private var mediaPreviewView: MediaPreviewView? private var action: (() -> Void)? private var currentMedia: DayMedia? init() { - self.buttonNode = HighlightableButtonNode() - self.highlightNode = ASImageNode() - self.titleNode = ImmediateTextNode() + self.button = HighlightableButton() + self.highlightView = UIImageView() + self.titleView = UIImageView() super.init(frame: CGRect()) - self.buttonNode.addSubnode(self.highlightNode) - self.buttonNode.addSubnode(self.titleNode) + self.button.addSubview(self.highlightView) + self.button.addSubview(self.titleView) - self.addSubnode(self.buttonNode) + self.addSubview(self.button) - self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder aDecoder: NSCoder) { @@ -258,58 +294,64 @@ private final class DayComponent: Component { func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.action = component.action - let shadowInset: CGFloat = 0.0 let diameter = min(availableSize.width, availableSize.height) + let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) let imageCache = environment[ImageCache.self] if component.media != nil { - self.highlightNode.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2)) + self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2)) } else if component.isCurrent { - self.highlightNode.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor) + self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor) } else { - self.highlightNode.image = nil + self.highlightView.image = nil } if self.currentMedia != component.media { self.currentMedia = component.media - if let mediaPreviewNode = self.mediaPreviewNode { - self.mediaPreviewNode = nil - mediaPreviewNode.removeFromSupernode() + if let mediaPreviewView = self.mediaPreviewView { + self.mediaPreviewView = nil + mediaPreviewView.removeFromSuperview() } if let media = component.media { - let mediaPreviewNode = MediaPreviewNode(context: component.context, message: media.message, media: media.media) - self.mediaPreviewNode = mediaPreviewNode - self.buttonNode.insertSubnode(mediaPreviewNode, belowSubnode: self.highlightNode) + let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media) + self.mediaPreviewView = mediaPreviewView + self.button.insertSubview(mediaPreviewView, belowSubview: self.highlightView) } } let titleColor: UIColor - let titleFont: UIFont + let titleFontSize: CGFloat + let titleFontIsSemibold: Bool if component.isCurrent || component.media != nil { titleColor = component.theme.list.itemCheckColors.foregroundColor - titleFont = Font.semibold(17.0) + titleFontSize = 17.0 + titleFontIsSemibold = true } else if component.isEnabled { titleColor = component.theme.list.itemPrimaryTextColor - titleFont = Font.regular(17.0) + titleFontSize = 17.0 + titleFontIsSemibold = false } else { titleColor = component.theme.list.itemDisabledTextColor - titleFont = Font.regular(17.0) + titleFontSize = 17.0 + titleFontIsSemibold = false } - self.titleNode.attributedText = NSAttributedString(string: component.title, font: titleFont, textColor: titleColor) - let titleSize = self.titleNode.updateLayout(availableSize) - transition.setFrame(view: self.highlightNode.view, frame: CGRect(origin: CGPoint(x: -shadowInset, y: -shadowInset), size: CGSize(width: availableSize.width + shadowInset * 2.0, height: availableSize.height + shadowInset * 2.0))) + let titleImage = imageCache.value.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title) + self.titleView.image = titleImage + let titleSize = titleImage.size - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + transition.setFrame(view: self.highlightView, frame: contentFrame) - self.buttonNode.frame = CGRect(origin: CGPoint(), size: availableSize) - self.buttonNode.isEnabled = component.isEnabled && component.media != nil + self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) - if let mediaPreviewNode = self.mediaPreviewNode { - mediaPreviewNode.frame = CGRect(origin: CGPoint(), size: availableSize) - mediaPreviewNode.updateLayout(size: availableSize, synchronousLoads: false) + self.button.frame = CGRect(origin: CGPoint(), size: availableSize) + self.button.isEnabled = component.isEnabled && component.media != nil + + if let mediaPreviewView = self.mediaPreviewView { + mediaPreviewView.frame = contentFrame + mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false) } return availableSize @@ -382,6 +424,7 @@ private final class MonthComponent: CombinedComponent { let weekdaySize: CGFloat = 46.0 let weekdaySpacing: CGFloat = 6.0 + let usableWeekdayWidth = floor((context.availableSize.width - sideInset * 2.0 - weekdaySpacing * 6.0) / 7.0) let weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0) let title = title.update( @@ -440,7 +483,7 @@ private final class MonthComponent: CombinedComponent { environment: { context.environment[ImageCache.self] }, - availableSize: CGSize(width: weekdaySize, height: weekdaySize), + availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize), transition: .immediate ) } @@ -843,7 +886,7 @@ public final class CalendarMessageScreen: ViewController { self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) //TODO:localize - self.navigationItem.setTitle("Jump to Date", animated: false) + self.navigationItem.setTitle("Calendar", animated: false) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index e86733f67a..df78de3be2 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -2,6 +2,9 @@ import Foundation import UIKit public final class ComponentHostView: UIView { + private var currentComponent: AnyComponent? + private var currentContainerSize: CGSize? + private var currentSize: CGSize? private var componentView: UIView? private(set) var isUpdating: Bool = false @@ -14,7 +17,16 @@ public final class ComponentHostView: UIView { } public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, containerSize: CGSize) -> CGSize { - self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize) + if let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { + if currentContainerSize == containerSize && currentComponent == component { + return currentSize + } + } + self.currentComponent = component + self.currentContainerSize = containerSize + let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize) + self.currentSize = size + return size } private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, containerSize: CGSize) -> CGSize { diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3d25047e66..72dc352b68 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -79,6 +79,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) + case mockICE(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -100,7 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .mockICE: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -169,14 +170,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 27 case .acceleratedStickers: return 29 - case .playerEmbedding: + case .mockICE: return 30 - case .playlistPlayback: + case .playerEmbedding: return 31 - case .voiceConference: + case .playlistPlayback: return 32 + case .voiceConference: + return 33 case let .preferredVideoCodec(index, _, _, _): - return 33 + index + return 34 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -749,6 +752,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .mockICE(value): + return ItemListSwitchItem(presentationData: presentationData, title: "mockICE", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.mockICE = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .playerEmbedding(value): return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -861,6 +874,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) + entries.append(.mockICE(experimentalSettings.mockICE)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) } diff --git a/submodules/DirectMediaImageCache/BUILD b/submodules/DirectMediaImageCache/BUILD index 94553e3ce7..96b64ee510 100644 --- a/submodules/DirectMediaImageCache/BUILD +++ b/submodules/DirectMediaImageCache/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TinyThumbnail:TinyThumbnail", "//submodules/Display:Display", "//submodules/FastBlur:FastBlur", + "//submodules/MozjpegBinding:MozjpegBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 0c56a5ed15..0c6c50e38d 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -6,6 +6,7 @@ import UIKit import TinyThumbnail import Display import FastBlur +import MozjpegBinding private func generateBlurredThumbnail(image: UIImage) -> UIImage? { let thumbnailContextSize = CGSize(width: 32.0, height: 32.0) @@ -22,6 +23,84 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? { return thumbnailContext.generateImage() } +private func storeImage(context: DrawingContext, to path: String) -> UIImage? { + if context.size.width <= 70.0 && context.size.height <= 70.0 { + guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else { + return nil + } + var header: UInt32 = 0xcaf1 + let _ = file.write(&header, count: 4) + var width: UInt16 = UInt16(context.size.width) + let _ = file.write(&width, count: 2) + var height: UInt16 = UInt16(context.size.height) + let _ = file.write(&height, count: 2) + var bytesPerRow: UInt16 = UInt16(context.bytesPerRow) + let _ = file.write(&bytesPerRow, count: 2) + + let _ = file.write(context.bytes, count: context.length) + + return context.generateImage() + } else { + guard let image = context.generateImage(), let resultData = image.jpegData(compressionQuality: 0.7) else { + return nil + } + let _ = try? resultData.write(to: URL(fileURLWithPath: path)) + return image + } +} + +private func loadImage(data: Data) -> UIImage? { + if data.count > 4 + 2 + 2 + 2 { + var header: UInt32 = 0 + withUnsafeMutableBytes(of: &header, { header in + data.copyBytes(to: header.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4) + }) + if header == 0xcaf1 { + var width: UInt16 = 0 + withUnsafeMutableBytes(of: &width, { width in + data.copyBytes(to: width.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 4 ..< (4 + 2)) + }) + var height: UInt16 = 0 + withUnsafeMutableBytes(of: &height, { height in + data.copyBytes(to: height.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2) ..< (4 + 2 + 2)) + }) + var bytesPerRow: UInt16 = 0 + withUnsafeMutableBytes(of: &bytesPerRow, { bytesPerRow in + data.copyBytes(to: bytesPerRow.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2 + 2) ..< (4 + 2 + 2 + 2)) + }) + + let imageData = data.subdata(in: (4 + 2 + 2 + 2) ..< data.count) + guard let dataProvider = CGDataProvider(data: imageData as CFData) else { + return nil + } + + if let image = CGImage( + width: Int(width), + height: Int(height), + bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent, + bitsPerPixel: DeviceGraphicsContextSettings.shared.bitsPerPixel, + bytesPerRow: Int(bytesPerRow), + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.opaqueBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) { + return UIImage(cgImage: image, scale: 1.0, orientation: .up) + } else { + return nil + } + } + } + + if let decompressedImage = decompressImage(data) { + return decompressedImage + } + + return UIImage(data: data) +} + public final class DirectMediaImageCache { public final class GetMediaResult { public let image: UIImage? @@ -65,16 +144,17 @@ public final class DirectMediaImageCache { } |> take(1)).start(next: { data in if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: dataValue) { - if let scaledImage = generateImage(CGSize(width: CGFloat(width), height: CGFloat(width)), contextGenerator: { size, context in - let filledSize = image.size.aspectFilled(size) - let imageRect = CGRect(origin: CGPoint(x: (size.width - filledSize.width) / 2.0, y: (size.height - filledSize.height) / 2.0), size: filledSize) + let scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width)) + let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true) + scaledContext.withFlippedContext { context in + let filledSize = image.size.aspectFilled(scaledSize) + let imageRect = CGRect(origin: CGPoint(x: (scaledSize.width - filledSize.width) / 2.0, y: (scaledSize.height - filledSize.height) / 2.0), size: filledSize) context.draw(image.cgImage!, in: imageRect) - }, scale: 1.0) { - if let resultData = scaledImage.jpegData(compressionQuality: 0.7) { - let _ = try? resultData.write(to: URL(fileURLWithPath: cachePath)) - subscriber.putNext(scaledImage) - subscriber.putCompletion() - } + } + + if let scaledImage = storeImage(context: scaledContext, to: cachePath) { + subscriber.putNext(scaledImage) + subscriber.putCompletion() } } }) @@ -113,14 +193,14 @@ public final class DirectMediaImageCache { } if let resource = resource { - if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = UIImage(data: data) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = loadImage(data: data) { return GetMediaResult(image: image, loadSignal: nil) } var blurredImage: UIImage? - if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = UIImage(data: data) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { blurredImage = image - } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = UIImage(data: data) { + } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { if let blurredImageValue = generateBlurredThumbnail(image: image) { blurredImage = blurredImageValue } diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 542cfeba7f..2130599be4 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -22,7 +22,7 @@ private class TimerTargetWrapper: NSObject { private let beginDelay: Double = 0.12 -private func cancelParentGestures(view: UIView) { +public func cancelParentGestures(view: UIView) { if let gestureRecognizers = view.gestureRecognizers { for recognizer in gestureRecognizers { recognizer.state = .failed @@ -31,6 +31,9 @@ private func cancelParentGestures(view: UIView) { if let node = (view as? ListViewBackingView)?.target { node.cancelSelection() } + if let node = view.asyncdisplaykit_node as? HighlightTrackingButtonNode { + node.highligthedChanged(false) + } if let superview = view.superview { cancelParentGestures(view: superview) } diff --git a/submodules/Display/Source/TransformImageNode.swift b/submodules/Display/Source/TransformImageNode.swift index 311ce62ee1..1cb1926a3f 100644 --- a/submodules/Display/Source/TransformImageNode.swift +++ b/submodules/Display/Source/TransformImageNode.swift @@ -206,3 +206,183 @@ open class TransformImageNode: ASDisplayNode { } } } + +open class TransformImageView: UIView { + public var imageUpdated: ((UIImage?) -> Void)? + public var contentAnimations: TransformImageNodeContentAnimations = [] + private var disposable = MetaDisposable() + + private var currentTransform: ((TransformImageArguments) -> DrawingContext?)? + private var currentArguments: TransformImageArguments? + private var argumentsPromise = ValuePromise(ignoreRepeated: true) + + private var overlayColor: UIColor? + private var overlayView: UIView? + + override public init(frame: CGRect) { + super.init(frame: frame) + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.accessibilityIgnoresInvertColors = true + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + 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 + } + + public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true) { + let argumentsPromise = self.argumentsPromise + + let data = combineLatest(signal, argumentsPromise.get()) + + let resultData: Signal<((TransformImageArguments) -> DrawingContext?, TransformImageArguments), NoError> + if attemptSynchronously { + resultData = data + } else { + resultData = data + |> deliverOn(Queue.concurrentDefaultQueue()) + } + + let result = resultData + |> mapToThrottled { transform, arguments -> Signal<((TransformImageArguments) -> DrawingContext?, TransformImageArguments, UIImage?)?, NoError> in + return deferred { + if let context = transform(arguments) { + return .single((transform, arguments, context.generateImage())) + } else { + return .single(nil) + } + } + } + + 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.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() + }) + } + + var imageUpdate: UIImage? + if let (transform, arguments, image) = next { + strongSelf.currentTransform = transform + strongSelf.currentArguments = arguments + strongSelf.layer.contents = image?.cgImage + imageUpdate = image + } + if let _ = strongSelf.overlayColor { + strongSelf.applyOverlayColor(animated: false) + } + if let imageUpdated = strongSelf.imageUpdated { + imageUpdated(imageUpdate) + } + } + } + if dispatchOnDisplayLink && !attemptSynchronously { + displayLinkDispatcher.dispatch { + apply() + } + } else { + apply() + } + })) + } + + public func asyncLayout() -> (TransformImageArguments) -> (() -> Void) { + let currentTransform = self.currentTransform + let currentArguments = self.currentArguments + return { [weak self] arguments in + let updatedImage: UIImage? + if currentArguments != arguments { + updatedImage = currentTransform?(arguments)?.generateImage() + } else { + updatedImage = nil + } + return { + guard let strongSelf = self else { + return + } + if let image = updatedImage { + strongSelf.layer.contents = image.cgImage + strongSelf.currentArguments = arguments + if let _ = strongSelf.overlayColor { + strongSelf.applyOverlayColor(animated: false) + } + } + strongSelf.argumentsPromise.set(arguments) + } + } + } + + public func setOverlayColor(_ color: UIColor?, animated: Bool) { + var updated = false + if let overlayColor = self.overlayColor, let color = color { + updated = !overlayColor.isEqual(color) + } else if (self.overlayColor != nil) != (color != nil) { + updated = true + } + if updated { + self.overlayColor = color + if let _ = self.overlayColor { + self.applyOverlayColor(animated: animated) + } else if let overlayView = self.overlayView { + self.overlayView = nil + if animated { + overlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak overlayView] _ in + overlayView?.removeFromSuperview() + }) + } else { + overlayView.removeFromSuperview() + } + } + } + } + + private func applyOverlayColor(animated: Bool) { + if let overlayColor = self.overlayColor { + if let contents = self.layer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + if let overlayView = self.overlayView { + (overlayView as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + overlayView.tintColor = overlayColor + } else { + let overlayView = UIImageView() + overlayView.image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + overlayView.tintColor = overlayColor + overlayView.frame = self.bounds + self.addSubview(overlayView) + self.overlayView = overlayView + } + } + } + } +} + diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 4afcac7889..9c48f8f71a 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -184,7 +184,7 @@ public final class ListMessageFileItemNode: ListMessageNode { private let playbackStatusDisposable = MetaDisposable() private let playbackStatus = Promise() - private var downloadStatusIconNode: DownloadIconNode + private var downloadStatusIconNode: DownloadIconNode? private var linearProgressNode: LinearProgressNode? private var context: AccountContext? @@ -216,15 +216,19 @@ public final class ListMessageFileItemNode: ListMessageNode { self.highlightedBackgroundNode.isLayerBacked = true self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false self.titleNode.isUserInteractionEnabled = false self.textNode = TextNode() + self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = false self.descriptionNode = TextNode() + self.descriptionNode.displaysAsynchronously = false self.descriptionNode.isUserInteractionEnabled = false self.descriptionProgressNode = ImmediateTextNode() + self.descriptionProgressNode.displaysAsynchronously = false self.descriptionProgressNode.isUserInteractionEnabled = false self.descriptionProgressNode.maximumNumberOfLines = 1 @@ -237,6 +241,7 @@ public final class ListMessageFileItemNode: ListMessageNode { self.extensionIconNode.displayWithoutProcessing = true self.extensionIconText = TextNode() + self.extensionIconText.displaysAsynchronously = false self.extensionIconText.isUserInteractionEnabled = false self.iconImageNode = TransformImageNode() @@ -246,8 +251,6 @@ public final class ListMessageFileItemNode: ListMessageNode { self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) self.iconStatusNode.isUserInteractionEnabled = false - self.downloadStatusIconNode = DownloadIconNode() - self.restrictionNode = ASDisplayNode() self.restrictionNode.isHidden = true @@ -275,6 +278,8 @@ public final class ListMessageFileItemNode: ListMessageNode { guard let strongSelf = self, let item = strongSelf.item else { return } + + cancelParentGestures(view: strongSelf.view) item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture) } @@ -647,9 +652,9 @@ public final class ListMessageFileItemNode: ListMessageNode { let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -700,6 +705,10 @@ public final class ListMessageFileItemNode: ListMessageNode { return (nodeLayout, { animation in if let strongSelf = self { + if strongSelf.downloadStatusIconNode == nil { + strongSelf.downloadStatusIconNode = DownloadIconNode(theme: item.presentationData.theme.theme) + } + let transition: ContainedViewLayoutTransition if animation.isAnimated { transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -742,8 +751,8 @@ public final class ListMessageFileItemNode: ListMessageNode { strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme) strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) - - strongSelf.downloadStatusIconNode.customColor = item.presentationData.theme.theme.list.itemAccentColor + + strongSelf.downloadStatusIconNode?.updateTheme(theme: item.presentationData.theme.theme) } if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { @@ -854,8 +863,10 @@ public final class ListMessageFileItemNode: ListMessageNode { } })) } - - transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0))) + + if let downloadStatusIconNode = strongSelf.downloadStatusIconNode { + transition.updateFrame(node: downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0))) + } if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) @@ -981,6 +992,10 @@ public final class ListMessageFileItemNode: ListMessageNode { override public func updateSelectionState(animated: Bool) { } + + public func cancelPreviewGesture() { + self.containerNode.cancelGesture() + } private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { guard let item = self.appliedItem else { @@ -1025,11 +1040,13 @@ public final class ListMessageFileItemNode: ListMessageNode { linearProgressNode.updateProgress(value: CGFloat(progress), completion: {}) var animated = true - if self.downloadStatusIconNode.supernode == nil { - animated = false - self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) + if let downloadStatusIconNode = self.downloadStatusIconNode { + if downloadStatusIconNode.supernode == nil { + animated = false + self.offsetContainerNode.addSubnode(downloadStatusIconNode) + } + downloadStatusIconNode.enqueueState(.pause, animated: animated) } - self.downloadStatusIconNode.enqueueState(.pause, animated: animated) case .Local: if let linearProgressNode = self.linearProgressNode { self.linearProgressNode = nil @@ -1039,8 +1056,10 @@ public final class ListMessageFileItemNode: ListMessageNode { }) }) } - if self.downloadStatusIconNode.supernode != nil { - self.downloadStatusIconNode.removeFromSupernode() + if let downloadStatusIconNode = self.downloadStatusIconNode { + if downloadStatusIconNode.supernode != nil { + downloadStatusIconNode.removeFromSupernode() + } } case .Remote: if let linearProgressNode = self.linearProgressNode { @@ -1049,12 +1068,14 @@ public final class ListMessageFileItemNode: ListMessageNode { linearProgressNode?.removeFromSupernode() }) } - var animated = true - if self.downloadStatusIconNode.supernode == nil { - animated = false - self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) + if let downloadStatusIconNode = self.downloadStatusIconNode { + var animated = true + if downloadStatusIconNode.supernode == nil { + animated = false + self.offsetContainerNode.addSubnode(downloadStatusIconNode) + } + downloadStatusIconNode.enqueueState(.download, animated: animated) } - self.downloadStatusIconNode.enqueueState(.download, animated: animated) } } else { if let linearProgressNode = self.linearProgressNode { @@ -1063,8 +1084,10 @@ public final class ListMessageFileItemNode: ListMessageNode { linearProgressNode?.removeFromSupernode() }) } - if self.downloadStatusIconNode.supernode != nil { - self.downloadStatusIconNode.removeFromSupernode() + if let downloadStatusIconNode = self.downloadStatusIconNode { + if downloadStatusIconNode.supernode != nil { + downloadStatusIconNode.removeFromSupernode() + } } } @@ -1090,7 +1113,7 @@ public final class ListMessageFileItemNode: ListMessageNode { transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize)) } - func activateMedia() { + public func activateMedia() { self.progressPressed() } @@ -1288,20 +1311,55 @@ private enum DownloadIconNodeState: Equatable { case pause } -private final class DownloadIconNode: ManagedAnimationNode { +private func generateDownloadIcon(color: UIColor) -> UIImage? { + let animation = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0)) + animation.customColor = color + animation.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + return animation.image +} + +private final class DownloadIconNode: ASImageNode { + private var customColor: UIColor private let duration: Double = 0.3 private var iconState: DownloadIconNodeState = .download + private var animationNode: ManagedAnimationNode? - init() { - super.init(size: CGSize(width: 18.0, height: 18.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + init(theme: PresentationTheme) { + self.customColor = theme.list.itemAccentColor + + super.init() + + self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: { + return generateDownloadIcon(color: theme.list.itemAccentColor) + }) + self.contentMode = .center + } + + func updateTheme(theme: PresentationTheme) { + self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: { + return generateDownloadIcon(color: theme.list.itemAccentColor) + }) + self.customColor = theme.list.itemAccentColor + self.animationNode?.customColor = self.customColor } func enqueueState(_ state: DownloadIconNodeState, animated: Bool) { guard self.iconState != state else { return } + + if self.animationNode == nil { + let animationNode = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0)) + self.animationNode = animationNode + animationNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 18.0, height: 18.0)) + animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + self.addSubnode(animationNode) + self.image = nil + } + + guard let animationNode = self.animationNode else { + return + } let previousState = self.iconState self.iconState = state @@ -1311,9 +1369,9 @@ private final class DownloadIconNode: ManagedAnimationNode { switch state { case .download: if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration)) + animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration)) } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) } case .pause: break @@ -1322,9 +1380,9 @@ private final class DownloadIconNode: ManagedAnimationNode { switch state { case .pause: if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) + animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01)) + animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01)) } case .download: break diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index 499a3e3304..e78181bb05 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -48,7 +48,7 @@ public final class ListMessageItem: ListViewItem { let chatLocation: ChatLocation let interaction: ListMessageItemInteraction let message: Message - let selection: ChatHistoryMessageSelection + public let selection: ChatHistoryMessageSelection let hintIsLink: Bool let isGlobalSearchResult: Bool diff --git a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h index c7142e4e5e..894c5238e5 100644 --- a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h +++ b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h @@ -3,3 +3,4 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage); NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size); +UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData); diff --git a/submodules/MozjpegBinding/Sources/MozjpegBinding.m b/submodules/MozjpegBinding/Sources/MozjpegBinding.m index 8138b309ad..efb4b5deb9 100644 --- a/submodules/MozjpegBinding/Sources/MozjpegBinding.m +++ b/submodules/MozjpegBinding/Sources/MozjpegBinding.m @@ -2,6 +2,7 @@ #import #import +#import static NSData *getHeaderPattern() { static NSData *value = nil; @@ -253,3 +254,80 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size) return serializedData; } + +UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData) { + long unsigned int jpegSize = sourceData.length; + unsigned char *_compressedImage = (unsigned char *)sourceData.bytes; + + int jpegSubsamp, width, height; + + tjhandle _jpegDecompressor = tjInitDecompress(); + + tjDecompressHeader2(_jpegDecompressor, _compressedImage, jpegSize, &width, &height, &jpegSubsamp); + + int sourceBytesPerRow = (3 * width + 31) & ~0x1F; + int targetBytesPerRow = (4 * width + 31) & ~0x1F; + + unsigned char *buffer = malloc(sourceBytesPerRow * height); + + tjDecompress2(_jpegDecompressor, _compressedImage, jpegSize, buffer, width, sourceBytesPerRow, height, TJPF_RGB, TJFLAG_FASTDCT | TJFLAG_FASTUPSAMPLE); + + tjDestroy(_jpegDecompressor); + + vImage_Buffer source; + source.width = width; + source.height = height; + source.rowBytes = sourceBytesPerRow; + source.data = buffer; + + vImage_Buffer target; + target.width = width; + target.height = height; + target.rowBytes = targetBytesPerRow; + + unsigned char *targetBuffer = malloc(targetBytesPerRow * height); + target.data = targetBuffer; + + vImageConvert_RGB888toARGB8888(&source, nil, 0xff, &target, false, kvImageDoNotTile); + + free(buffer); + + vImage_Buffer permuteTarget; + permuteTarget.width = width; + permuteTarget.height = height; + permuteTarget.rowBytes = targetBytesPerRow; + + unsigned char *permuteTargetBuffer = malloc(targetBytesPerRow * height); + permuteTarget.data = permuteTargetBuffer; + + const uint8_t permuteMap[4] = {3,2,1,0}; + vImagePermuteChannels_ARGB8888(&target, &permuteTarget, permuteMap, kvImageDoNotTile); + + free(targetBuffer); + + NSData *resultData = [[NSData alloc] initWithBytesNoCopy:permuteTargetBuffer length:targetBytesPerRow * height deallocator:^(void * _Nonnull bytes, __unused NSUInteger length) { + free(bytes); + }]; + + CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)resultData); + + static CGColorSpaceRef imageColorSpace; + static CGBitmapInfo bitmapInfo; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext(); + imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage)); + bitmapInfo = CGImageGetBitmapInfo(refImage.CGImage); + UIGraphicsEndImageContext(); + }); + + CGImageRef cgImg = CGImageCreate(width, height, 8, 32, targetBytesPerRow, imageColorSpace, bitmapInfo, dataProvider, NULL, true, kCGRenderingIntentDefault); + + CGDataProviderRelease(dataProvider); + + UIImage *resultImage = [[UIImage alloc] initWithCGImage:cgImg]; + CGImageRelease(cgImg); + + return resultImage; +} diff --git a/submodules/SparseItemGrid/BUILD b/submodules/SparseItemGrid/BUILD index 1075f093b6..b7d80e82a0 100644 --- a/submodules/SparseItemGrid/BUILD +++ b/submodules/SparseItemGrid/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", + "//submodules/AnimationUI:AnimationUI", ], visibility = [ "//visibility:public", diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 319563c023..5e12a6caed 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -13,17 +13,32 @@ private let nullAction = NullActionClass() public protocol SparseItemGridLayer: CALayer { func update(size: CGSize) + func needsShimmer() -> Bool +} + +public protocol SparseItemGridView: UIView { + func update(size: CGSize) + func needsShimmer() -> Bool +} + +public protocol SparseItemGridDisplayItem: AnyObject { + var layer: SparseItemGridLayer? { get } + var view: SparseItemGridView? { get } } public protocol SparseItemGridBinding: AnyObject { - func createLayer() -> SparseItemGridLayer - func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer]) + func createLayer() -> SparseItemGridLayer? + func createView() -> SparseItemGridView? + func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], synchronous: Bool) func unbindLayer(layer: SparseItemGridLayer) func scrollerTextForTag(tag: Int32) -> String? func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal func onTap(item: SparseItemGrid.Item) func onTagTap() func didScroll() + func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) + func onBeginFastScrolling() + func getShimmerColors() -> SparseItemGrid.ShimmerColors } private func binarySearch(_ inputArr: [SparseItemGrid.Item], searchItem: Int) -> (index: Int?, lowerBound: Int?, upperBound: Int?) { @@ -78,24 +93,69 @@ private func binarySearch(_ inputArr: [SparseItemGrid.HoleAnchor], searchItem: I } } -public final class SparseItemGrid: ASDisplayNode { - public final class ShimmerLayer: CAGradientLayer { - override public init() { - super.init() +private final class Shimmer { + private var image: UIImage? + private var colors: SparseItemGrid.ShimmerColors = SparseItemGrid.ShimmerColors(background: 0, foreground: 0) - self.backgroundColor = UIColor(white: 0.9, alpha: 1.0).cgColor + func update(colors: SparseItemGrid.ShimmerColors, layer: CALayer, containerSize: CGSize, frame: CGRect) { + if self.colors != colors { + self.colors = colors + + self.image = generateImage(CGSize(width: 1.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: colors.background).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = UIColor(argb: colors.foreground).withAlphaComponent(0.0).cgColor + let peakColor = UIColor(argb: colors.foreground).cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) } - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + if let image = self.image { + layer.contents = image.cgImage - override public func action(forKey event: String) -> CAAction? { + let shiftedContentsRect = CGRect(origin: CGPoint(x: frame.minX / containerSize.width, y: frame.minY / containerSize.height), size: CGSize(width: frame.width / containerSize.width, height: frame.height / containerSize.height)) + let _ = shiftedContentsRect + layer.contentsRect = shiftedContentsRect + + if layer.animation(forKey: "shimmer") == nil { + let animation = CABasicAnimation(keyPath: "contentsRect.origin.y") + animation.fromValue = 1.0 as NSNumber + animation.toValue = -1.0 as NSNumber + animation.isAdditive = true + animation.repeatCount = .infinity + animation.duration = 0.8 + animation.beginTime = 1.0 + layer.add(animation, forKey: "shimmer") + } + } + } + + final class Layer: CALayer { + override func action(forKey event: String) -> CAAction? { return nullAction } + } +} - func update(size: CGSize) { - self.endPoint = CGPoint(x: 0.0, y: size.height) +public final class SparseItemGrid: ASDisplayNode { + public struct ShimmerColors: Equatable { + public var background: UInt32 + public var foreground: UInt32 + + public init(background: UInt32, foreground: UInt32) { + self.background = background + self.foreground = foreground } } @@ -258,40 +318,84 @@ public final class SparseItemGrid: ASDisplayNode { } private final class Viewport: ASDisplayNode, UIScrollViewDelegate { - final class VisibleItemLayer { - let layer: SparseItemGridLayer + final class VisibleItem: SparseItemGridDisplayItem { + let layer: SparseItemGridLayer? + let view: SparseItemGridView? - init(layer: SparseItemGridLayer) { + init(layer: SparseItemGridLayer?, view: SparseItemGridView?) { self.layer = layer + self.view = view + } + + var displayLayer: CALayer { + if let layer = self.layer { + return layer + } else if let view = self.view { + return view.layer + } else { + preconditionFailure() + } + } + + var frame: CGRect { + get { + return self.displayLayer.frame + } set(value) { + if let layer = self.layer { + layer.frame = value + } else if let view = self.view { + view.frame = value + } else { + preconditionFailure() + } + } + } + + var needsShimmer: Bool { + if let layer = self.layer { + return layer.needsShimmer() + } else if let view = self.view { + return view.needsShimmer() + } else { + preconditionFailure() + } } } final class Layout { let containerLayout: ContainerLayout - let itemSize: CGFloat + let itemSize: CGSize let itemSpacing: CGFloat let lastItemSize: CGFloat let itemsPerRow: Int init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) { self.containerLayout = containerLayout - self.itemSpacing = 1.0 + if let fixedItemHeight = containerLayout.fixedItemHeight { + self.itemsPerRow = 1 + self.itemSize = CGSize(width: containerLayout.size.width, height: fixedItemHeight) + self.lastItemSize = containerLayout.size.width + self.itemSpacing = 0.0 + } else { + self.itemSpacing = 1.0 - let width = containerLayout.size.width - let baseItemWidth = floor(min(150.0, width / 3.0)) - let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth - let itemsPerRow = floor(width / unclippedItemWidth) - self.itemsPerRow = Int(itemsPerRow) - self.itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow) + let width = containerLayout.size.width + let baseItemWidth = floor(min(150.0, width / 3.0)) + let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth + let itemsPerRow = floor(width / unclippedItemWidth) + self.itemsPerRow = Int(itemsPerRow) + let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow) + self.itemSize = CGSize(width: itemSize, height: itemSize) - self.lastItemSize = width - (self.itemSize + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) + self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) + } } func frame(at index: Int) -> CGRect { let row = index / self.itemsPerRow let column = index % self.itemsPerRow - return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize + self.itemSpacing), y: CGFloat(row) * (self.itemSize + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize, height: itemSize)) + return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: self.containerLayout.insets.top + CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height)) } func contentHeight(count: Int) -> CGFloat { @@ -299,9 +403,10 @@ public final class SparseItemGrid: ASDisplayNode { } func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) { - var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.containerLayout.insets.top) + var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing))) minVisibleRow = max(0, minVisibleRow) - let maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing))) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing))) let minVisibleIndex = minVisibleRow * self.itemsPerRow let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) @@ -313,16 +418,22 @@ public final class SparseItemGrid: ASDisplayNode { let zoomLevel: ZoomLevel private let scrollView: UIScrollView + private let shimmer: Shimmer var layout: Layout? var items: Items? - var visibleItems: [AnyHashable: VisibleItemLayer] = [:] - var visiblePlaceholders: [ShimmerLayer] = [] + var visibleItems: [AnyHashable: VisibleItem] = [:] + var visiblePlaceholders: [Shimmer.Layer] = [] private var scrollingArea: SparseItemGridScrollingArea? + private var currentScrollingTag: Int32? private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void private var ignoreScrolling: Bool = false + private var isFastScrolling: Bool = false + + private var previousScrollOffset: CGFloat = 0.0 + var coveringInsetOffset: CGFloat = 0.0 init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void) { self.zoomLevel = zoomLevel @@ -338,6 +449,8 @@ public final class SparseItemGrid: ASDisplayNode { self.scrollView.delaysContentTouches = false self.scrollView.clipsToBounds = false + self.shimmer = Shimmer() + super.init() self.anchorPoint = CGPoint() @@ -351,13 +464,105 @@ public final class SparseItemGrid: ASDisplayNode { self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel) self.items = items - self.updateVisibleItems(resetScrolling: true, restoreScrollPosition: restoreScrollPosition) + self.updateVisibleItems(resetScrolling: true, synchronous: false, restoreScrollPosition: restoreScrollPosition) } } + @objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.items?.itemBinding.didScroll() + } + @objc func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.updateVisibleItems(resetScrolling: false, restoreScrollPosition: nil) + self.updateVisibleItems(resetScrolling: false, synchronous: true, restoreScrollPosition: nil) + + if let layout = self.layout, let items = self.items { + let offset = scrollView.contentOffset.y + let delta = offset - self.previousScrollOffset + self.previousScrollOffset = offset + + if self.isFastScrolling { + if offset <= layout.containerLayout.insets.top { + var coveringInsetOffset = self.coveringInsetOffset + delta + if coveringInsetOffset < 0.0 { + coveringInsetOffset = 0.0 + } + if coveringInsetOffset > layout.containerLayout.insets.top { + coveringInsetOffset = layout.containerLayout.insets.top + } + if offset <= 0.0 { + coveringInsetOffset = 0.0 + } + if coveringInsetOffset < self.coveringInsetOffset { + self.coveringInsetOffset = coveringInsetOffset + items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate) + } + } + } else { + var coveringInsetOffset = self.coveringInsetOffset + delta + if coveringInsetOffset < 0.0 { + coveringInsetOffset = 0.0 + } + if coveringInsetOffset > layout.containerLayout.insets.top { + coveringInsetOffset = layout.containerLayout.insets.top + } + if offset <= 0.0 { + coveringInsetOffset = 0.0 + } + if coveringInsetOffset != self.coveringInsetOffset { + self.coveringInsetOffset = coveringInsetOffset + items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate) + } + } + } + } + } + + @objc func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.snapCoveringInsetOffset() + } + } + + @objc func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !self.ignoreScrolling { + if !decelerate { + self.snapCoveringInsetOffset() + } + } + } + + @objc func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.snapCoveringInsetOffset() + } + } + + private func snapCoveringInsetOffset() { + if let layout = self.layout, let items = self.items { + let offset = self.scrollView.contentOffset.y + if offset < layout.containerLayout.insets.top { + if offset <= layout.containerLayout.insets.top / 2.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } else { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: layout.containerLayout.insets.top), animated: true) + } + } else { + var coveringInsetOffset = self.coveringInsetOffset + if coveringInsetOffset > layout.containerLayout.insets.top / 2.0 { + coveringInsetOffset = layout.containerLayout.insets.top + } else { + coveringInsetOffset = 0.0 + } + if offset <= 0.0 { + coveringInsetOffset = 0.0 + } + + if coveringInsetOffset != self.coveringInsetOffset { + self.coveringInsetOffset = coveringInsetOffset + items.itemBinding.coveringInsetOffsetUpdated(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } } } @@ -369,7 +574,7 @@ public final class SparseItemGrid: ASDisplayNode { let localPoint = self.scrollView.convert(point, from: self.view) for (id, visibleItem) in self.visibleItems { - if visibleItem.layer.frame.contains(localPoint) { + if visibleItem.frame.contains(localPoint) { for item in items.items { if item.id == id { return item @@ -391,7 +596,7 @@ public final class SparseItemGrid: ASDisplayNode { var closestItem: (CGFloat, AnyHashable)? for (id, visibleItem) in self.visibleItems { - let itemCenter = visibleItem.layer.frame.center + let itemCenter = visibleItem.frame.center let distanceX = itemCenter.x - localPoint.x let distanceY = itemCenter.y - localPoint.y let distance2 = distanceX * distanceX + distanceY * distanceY @@ -446,12 +651,22 @@ public final class SparseItemGrid: ASDisplayNode { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } - private func updateVisibleItems(resetScrolling: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) { + func scrollToTop() -> Bool { + if self.scrollView.contentOffset.y > 0.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + return true + } else { + return false + } + } + + private func updateVisibleItems(resetScrolling: Bool, synchronous: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) { guard let layout = self.layout, let items = self.items else { return } let contentHeight = layout.contentHeight(count: items.count) + let shimmerColors = items.itemBinding.getShimmerColors() if resetScrolling { if !self.scrollView.bounds.isEmpty { @@ -494,49 +709,65 @@ public final class SparseItemGrid: ASDisplayNode { var usedPlaceholderCount = 0 if !items.items.isEmpty { var bindItems: [Item] = [] - var bindLayers: [SparseItemGridLayer] = [] - var updateLayers: [SparseItemGridLayer] = [] + var bindLayers: [SparseItemGridDisplayItem] = [] + var updateLayers: [SparseItemGridDisplayItem] = [] let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) for index in visibleRange.minIndex ... visibleRange.maxIndex { if let item = items.item(at: index) { - let itemLayer: VisibleItemLayer + let itemLayer: VisibleItem if let current = self.visibleItems[item.id] { itemLayer = current - updateLayers.append(itemLayer.layer) + updateLayers.append(itemLayer) } else { - itemLayer = VisibleItemLayer(layer: items.itemBinding.createLayer()) + itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView()) self.visibleItems[item.id] = itemLayer bindItems.append(item) - bindLayers.append(itemLayer.layer) + bindLayers.append(itemLayer) - self.scrollView.layer.addSublayer(itemLayer.layer) + if let layer = itemLayer.layer { + self.scrollView.layer.addSublayer(layer) + } else if let view = itemLayer.view { + self.scrollView.addSubview(view) + } } validIds.insert(item.id) - itemLayer.layer.frame = layout.frame(at: index) - } else { - let placeholderLayer: ShimmerLayer + itemLayer.frame = layout.frame(at: index) + } else if layout.containerLayout.fixedItemHeight == nil { + let placeholderLayer: Shimmer.Layer if self.visiblePlaceholders.count > usedPlaceholderCount { placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount] } else { - placeholderLayer = ShimmerLayer() + placeholderLayer = Shimmer.Layer() self.scrollView.layer.addSublayer(placeholderLayer) self.visiblePlaceholders.append(placeholderLayer) } - placeholderLayer.frame = layout.frame(at: index) + let itemFrame = layout.frame(at: index) + placeholderLayer.frame = itemFrame + self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) usedPlaceholderCount += 1 } } if !bindItems.isEmpty { - items.itemBinding.bindLayers(items: bindItems, layers: bindLayers) + items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, synchronous: synchronous) } - for layer in updateLayers { - layer.update(size: layer.bounds.size) + for item in updateLayers { + let item = item as! VisibleItem + if let layer = item.layer { + layer.update(size: layer.frame.size) + } else if let view = item.view { + view.update(size: layer.frame.size) + } + + if item.needsShimmer { + let itemFrame = layer.frame + self.shimmer.update(colors: shimmerColors, layer: item.displayLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) + } } } @@ -547,9 +778,13 @@ public final class SparseItemGrid: ASDisplayNode { } } for id in removeIds { - if let itemLayer = self.visibleItems.removeValue(forKey: id) { - items.itemBinding.unbindLayer(layer: itemLayer.layer) - itemLayer.layer.removeFromSuperlayer() + if let item = self.visibleItems.removeValue(forKey: id) { + if let layer = item.layer { + items.itemBinding.unbindLayer(layer: layer) + layer.removeFromSuperlayer() + } else if let view = item.view { + view.removeFromSuperview() + } } } @@ -600,8 +835,17 @@ public final class SparseItemGrid: ASDisplayNode { guard let strongSelf = self else { return nil } + strongSelf.items?.itemBinding.onBeginFastScrolling() return strongSelf.scrollView } + scrollingArea.setContentOffset = { [weak self] offset in + guard let strongSelf = self else { + return + } + strongSelf.isFastScrolling = true + strongSelf.scrollView.setContentOffset(offset, animated: false) + strongSelf.isFastScrolling = false + } self.updateScrollingArea() } } @@ -624,13 +868,20 @@ public final class SparseItemGrid: ASDisplayNode { } if let scrollingArea = self.scrollingArea { + let dateString = tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } + if self.currentScrollingTag != tag { + self.currentScrollingTag = tag + if scrollingArea.isDragging { + scrollingArea.feedbackTap() + } + } scrollingArea.update( containerSize: layout.containerLayout.size, containerInsets: layout.containerLayout.insets, contentHeight: contentHeight, contentOffset: self.scrollView.bounds.minY, isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating, - dateString: tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } ?? "", + dateString: dateString ?? "", transition: .immediate ) } @@ -740,8 +991,10 @@ public final class SparseItemGrid: ASDisplayNode { var insets: UIEdgeInsets var scrollIndicatorInsets: UIEdgeInsets var lockScrollingAtTop: Bool + var fixedItemHeight: CGFloat? } + private var tapRecognizer: UITapGestureRecognizer? private var pinchRecognizer: UIPinchGestureRecognizer? private var containerLayout: ContainerLayout? @@ -754,6 +1007,13 @@ public final class SparseItemGrid: ASDisplayNode { private var isLoadingHole: Bool = false private let loadingHoleDisposable = MetaDisposable() + public var coveringInsetOffset: CGFloat { + if let currentViewport = self.currentViewport { + return currentViewport.coveringInsetOffset + } + return 0.0 + } + override public init() { self.scrollingArea = SparseItemGridScrollingArea() @@ -762,6 +1022,7 @@ public final class SparseItemGrid: ASDisplayNode { self.clipsToBounds = true let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer self.view.addGestureRecognizer(tapRecognizer) let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) @@ -936,12 +1197,15 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, items: Items) { - let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop) + public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items) { + let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight) self.containerLayout = containerLayout self.items = items self.scrollingArea.isHidden = lockScrollingAtTop + self.tapRecognizer?.isEnabled = fixedItemHeight == nil + self.pinchRecognizer?.isEnabled = fixedItemHeight == nil + if self.currentViewport == nil { let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in guard let strongSelf = self else { @@ -1063,12 +1327,12 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func forEachVisibleItem(_ f: (SparseItemGridLayer) -> Void) { + public func forEachVisibleItem(_ f: (SparseItemGridDisplayItem) -> Void) { guard let currentViewport = self.currentViewport else { return } for (_, itemLayer) in currentViewport.visibleItems { - f(itemLayer.layer) + f(itemLayer) } } @@ -1086,7 +1350,18 @@ public final class SparseItemGrid: ASDisplayNode { currentViewport.scrollToItem(at: index) } + public func scrollToTop() -> Bool { + guard let currentViewport = self.currentViewport else { + return false + } + return currentViewport.scrollToTop() + } + public func addToTransitionSurface(view: UIView) { self.view.insertSubview(view, belowSubview: self.scrollingArea.view) } + + public func updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip) { + self.scrollingArea.displayTooltip = tooltip + } } diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 6c9dc6e9f0..ef21e2e6e8 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -4,6 +4,263 @@ import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit +import AnimationUI + +public final class MultilineText: Component { + public let text: String + public let font: UIFont + public let color: UIColor + + public init( + text: String, + font: UIFont, + color: UIColor + ) { + self.text = text + self.font = font + self.color = color + } + + public static func ==(lhs: MultilineText, rhs: MultilineText) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.font != rhs.font { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private let text: ImmediateTextNode + + init() { + self.text = ImmediateTextNode() + self.text.maximumNumberOfLines = 0 + + super.init(frame: CGRect()) + + self.addSubnode(self.text) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: MultilineText, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + self.text.attributedText = NSAttributedString(string: component.text, font: component.font, textColor: component.color, paragraphAlignment: nil) + let textSize = self.text.updateLayout(availableSize) + transition.setFrame(view: self.text.view, frame: CGRect(origin: CGPoint(), size: textSize)) + + return textSize + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} + +public final class LottieAnimationComponent: Component { + public let name: String + + public init( + name: String + ) { + self.name = name + } + + public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool { + if lhs.name != rhs.name { + return false + } + return true + } + + public final class View: UIView { + private var animationNode: AnimationNode? + private var currentName: String? + + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: LottieAnimationComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + if self.currentName != component.name { + self.currentName = component.name + + if let animationNode = self.animationNode { + animationNode.removeFromSupernode() + self.animationNode = nil + } + + let animationNode = AnimationNode(animation: component.name, colors: [:], scale: 1.0) + self.animationNode = animationNode + self.addSubnode(animationNode) + + animationNode.play() + } + + if let animationNode = self.animationNode { + let preferredSize = animationNode.preferredSize() + return preferredSize ?? CGSize(width: 32.0, height: 32.0) + } else { + return CGSize() + } + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} + +public final class TooltipComponent: Component { + public let icon: AnyComponent? + public let content: AnyComponent + public let arrowLocation: CGRect + + public init( + icon: AnyComponent?, + content: AnyComponent, + arrowLocation: CGRect + ) { + self.icon = icon + self.content = content + self.arrowLocation = arrowLocation + } + + public static func ==(lhs: TooltipComponent, rhs: TooltipComponent) -> Bool { + if lhs.icon != rhs.icon { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.arrowLocation != rhs.arrowLocation { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: UIView + private let backgroundViewMask: UIImageView + private var icon: ComponentHostView? + private let content: ComponentHostView + + init() { + self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + self.backgroundViewMask = UIImageView() + + self.backgroundViewMask.image = generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.black.cgColor) + let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ") + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 34) + + self.content = ComponentHostView() + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + self.backgroundView.mask = self.backgroundViewMask + self.addSubview(self.content) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: TooltipComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + let insets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) + let spacing: CGFloat = 8.0 + + var iconSize: CGSize? + if let icon = component.icon { + let iconView: ComponentHostView + if let current = self.icon { + iconView = current + } else { + iconView = ComponentHostView() + self.icon = iconView + self.addSubview(iconView) + } + iconSize = iconView.update( + transition: transition, + component: icon, + environment: {}, + containerSize: availableSize + ) + } else if let icon = self.icon { + self.icon = nil + icon.removeFromSuperview() + } + + var contentLeftInset: CGFloat = 0.0 + if let iconSize = iconSize { + contentLeftInset += iconSize.width + spacing + } + + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: CGSize(width: min(200.0, availableSize.width - contentLeftInset), height: availableSize.height) + ) + + var innerContentHeight = contentSize.height + if let iconSize = iconSize, iconSize.height > innerContentHeight { + innerContentHeight = iconSize.height + } + + let combinedContentSize = CGSize(width: insets.left + insets.right + contentLeftInset + contentSize.width, height: insets.top + insets.bottom + innerContentHeight) + var contentRect = CGRect(origin: CGPoint(x: component.arrowLocation.minX - combinedContentSize.width, y: component.arrowLocation.maxY), size: combinedContentSize) + if contentRect.minX < 0.0 { + contentRect.origin.x = component.arrowLocation.maxX + } + if contentRect.minY < 0.0 { + contentRect.origin.y = component.arrowLocation.minY - contentRect.height + } + + let maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 4.0)) + + self.backgroundView.frame = maskedBackgroundFrame + self.backgroundViewMask.frame = CGRect(origin: CGPoint(), size: maskedBackgroundFrame.size) + + if let iconSize = iconSize, let icon = self.icon { + transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize)) + } + transition.setFrame(view: self.content, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left + contentLeftInset, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - contentSize.height) / 2.0)), size: contentSize)) + + return availableSize + } + } + + public func makeView() -> View { + return View() + } + + public 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 RoundedRectangle: Component { let color: UIColor @@ -134,7 +391,7 @@ private final class ShadowRoundedRectangle: Component { context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(component.color.cgColor) - context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 5.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor) + context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 4.0, color: UIColor(white: 0.0, alpha: 0.2).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0))) })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowInset * 2.0) / 2, topCapHeight: Int(diameter + shadowInset * 2.0) / 2) @@ -324,6 +581,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private let dateIndicator: ComponentHostView private let lineIndicator: ComponentHostView + + private var displayedTooltip: Bool = false + private var lineTooltip: ComponentHostView? + + private var containerSize: CGSize? private var indicatorPosition: CGFloat? private var scrollIndicatorHeight: CGFloat? @@ -336,6 +598,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private var activityTimer: SwiftSignalKit.Timer? public var beginScrolling: (() -> UIScrollView?)? + public var setContentOffset: ((CGPoint) -> Void)? public var openCurrentDate: (() -> Void)? private var offsetBarTimer: SwiftSignalKit.Timer? @@ -350,6 +613,20 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } private var projectionData: ProjectionData? + public struct DisplayTooltip { + public var animation: String? + public var text: String + public var completed: () -> Void + + public init(animation: String?, text: String, completed: @escaping () -> Void) { + self.animation = animation + self.text = text + self.completed = completed + } + } + + public var displayTooltip: DisplayTooltip? + override public init() { self.dateIndicator = ComponentHostView() self.lineIndicator = ComponentHostView() @@ -399,10 +676,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { if let scrollView = strongSelf.beginScrolling?() { strongSelf.draggingScrollView = scrollView strongSelf.scrollingInitialOffset = scrollView.contentOffset.y - scrollView.setContentOffset(scrollView.contentOffset, animated: false) + strongSelf.setContentOffset?(scrollView.contentOffset) } - strongSelf.updateActivityTimer() + strongSelf.updateActivityTimer(isScrolling: false) }, ended: { [weak self] in guard let strongSelf = self else { @@ -424,7 +701,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { strongSelf.updateLineIndicator(transition: transition) - strongSelf.updateActivityTimer() + strongSelf.updateActivityTimer(isScrolling: false) }, moved: { [weak self] relativeOffset in guard let strongSelf = self else { @@ -454,7 +731,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { offset = scrollView.contentSize.height - scrollView.bounds.height } - scrollView.setContentOffset(CGPoint(x: 0.0, y: offset), animated: false) + strongSelf.setContentOffset?(CGPoint(x: 0.0, y: offset)) let _ = scrollView let _ = projectionData } @@ -473,6 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { self.updateLineIndicator(transition: transition) } + func feedbackTap() { + self.hapticFeedback.tap() + } + public func update( containerSize: CGSize, containerInsets: UIEdgeInsets, @@ -482,8 +763,15 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { dateString: String, transition: ContainedViewLayoutTransition ) { + self.containerSize = containerSize + + if self.dateIndicator.alpha.isZero { + let transition: ContainedViewLayoutTransition = .immediate + transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint()) + } + if isScrolling { - self.updateActivityTimer() + self.updateActivityTimer(isScrolling: true) } let indicatorSize = self.dateIndicator.update( @@ -508,18 +796,18 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } let indicatorVerticalInset: CGFloat = 3.0 - let topIndicatorInset: CGFloat = indicatorVerticalInset + let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom - let scrollIndicatorHeight = max(35.0, ceil(scrollIndicatorHeightFraction * containerSize.height)) + let scrollIndicatorHeight = max(44.0, ceil(scrollIndicatorHeightFraction * containerSize.height)) let indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height))) let indicatorTopPosition = topIndicatorInset let indicatorBottomPosition = containerSize.height - bottomIndicatorInset - scrollIndicatorHeight - let dateIndicatorTopPosition = topIndicatorInset - let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - indicatorSize.height + let dateIndicatorTopPosition = topIndicatorInset + 4.0 + let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - 4.0 - indicatorSize.height self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction self.scrollIndicatorHeight = scrollIndicatorHeight @@ -535,11 +823,18 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { transition.updateFrame(view: self.dateIndicator, frame: CGRect(origin: CGPoint(x: containerSize.width - 12.0 - indicatorSize.width, y: dateIndicatorPosition), size: indicatorSize)) if isScrolling { - self.dateIndicator.alpha = 1.0 - self.lineIndicator.alpha = 1.0 + 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) } + self.updateLineTooltip(containerSize: containerSize) + self.updateLineIndicator(transition: transition) + + if isScrolling { + self.displayTooltipOnFirstScroll() + } } private func updateLineIndicator(transition: ContainedViewLayoutTransition) { @@ -547,7 +842,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { return } - let lineIndicatorSize = CGSize(width: self.isDragging ? 6.0 : 3.0, height: scrollIndicatorHeight) + let lineIndicatorSize = CGSize(width: (self.isDragging || self.lineTooltip != nil) ? 6.0 : 3.0, height: scrollIndicatorHeight) let mappedTransition: Transition switch transition { case .immediate: @@ -567,7 +862,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { transition.updateFrame(view: self.lineIndicator, frame: CGRect(origin: CGPoint(x: self.bounds.size.width - 3.0 - lineIndicatorSize.width, y: indicatorPosition), size: lineIndicatorSize)) } - private func updateActivityTimer() { + private func updateActivityTimer(isScrolling: Bool) { self.activityTimer?.invalidate() if self.isDragging { @@ -582,11 +877,71 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { 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) + + if let lineTooltip = strongSelf.lineTooltip { + strongSelf.lineTooltip = nil + lineTooltip.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak lineTooltip] _ in + lineTooltip?.removeFromSuperview() + }) + } }, queue: .mainQueue()) self.activityTimer?.start() } } + private func displayTooltipOnFirstScroll() { + guard let displayTooltip = self.displayTooltip else { + return + } + if self.displayedTooltip { + return + } + self.displayedTooltip = true + + let lineTooltip = ComponentHostView() + self.lineTooltip = lineTooltip + self.view.addSubview(lineTooltip) + + if let containerSize = self.containerSize { + self.updateLineTooltip(containerSize: containerSize) + } + + lineTooltip.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let transition: ContainedViewLayoutTransition = .immediate + transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint(x: -3.0, y: 0.0)) + + displayTooltip.completed() + } + + private func updateLineTooltip(containerSize: CGSize) { + guard let displayTooltip = self.displayTooltip else { + return + } + guard let lineTooltip = self.lineTooltip else { + return + } + let lineTooltipSize = lineTooltip.update( + transition: .immediate, + component: AnyComponent(TooltipComponent( + icon: displayTooltip.animation.flatMap { animation in + AnyComponent(LottieAnimationComponent( + name: animation + )) + }, + content: AnyComponent(MultilineText( + text: displayTooltip.text, + font: Font.regular(13.0), + color: .white + )), + arrowLocation: self.lineIndicator.frame.insetBy(dx: -3.0, dy: -8.0) + )), + environment: {}, + containerSize: containerSize + ) + lineTooltip.frame = CGRect(origin: CGPoint(), size: lineTooltipSize) + } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.dateIndicator.alpha <= 0.01 { return nil diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 0eed053664..8f34551fc4 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -682,8 +682,10 @@ public final class PresentationCallImpl: PresentationCall { self.audioSessionShouldBeActive.set(true) if let _ = audioSessionControl, !wasActive || previousControl == nil { let logName = "\(id.id)_\(id.accessHash)" + + let updatedConnections = connections - let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: self.audioSessionActive.get(), logName: logName, preferredVideoCodec: self.preferredVideoCodec) + let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: self.audioSessionActive.get(), logName: logName, preferredVideoCodec: self.preferredVideoCodec) self.ongoingContext = ongoingContext ongoingContext.setIsMuted(self.isMutedValue) if let requestedVideoAspect = self.requestedVideoAspect { diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 65ad4a5c5d..52638a6c16 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -232,6 +232,11 @@ private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessio public struct CallSessionConnectionSet { public let primary: CallSessionConnection public let alternatives: [CallSessionConnection] + + public init(primary: CallSessionConnection, alternatives: [CallSessionConnection]) { + self.primary = primary + self.alternatives = alternatives + } } private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.PhoneConnection]) -> CallSessionConnectionSet { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index a41f16b80d..3c3e3f247d 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -160,6 +160,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case chatSpecificThemeLightPreviewTip = 26 case chatSpecificThemeDarkPreviewTip = 27 case interactiveEmojiSyncTip = 28 + case sharedMediaScrollingTooltip = 29 + case sharedMediaFastScrollingTooltip = 30 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -324,6 +326,14 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: peerInviteRequestsNamespace), key: noticeKey(peerId: peerId, key: 0)) } + + static func sharedMediaScrollingTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.sharedMediaScrollingTooltip.key) + } + + static func sharedMediaFastScrollingTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.sharedMediaFastScrollingTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -893,6 +903,54 @@ public struct ApplicationSpecificNotice { } } } + + public static func getSharedMediaScrollingTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementSharedMediaScrollingTooltip(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + currentValue += count + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip(), entry) + } + } + } + + public static func getSharedMediaFastScrollingTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementSharedMediaFastScrollingTooltip(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + currentValue += count + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip(), entry) + } + } + } public static func dismissedTrendingStickerPacks(accountManager: AccountManager) -> Signal<[Int64]?, NoError> { return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks()) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 21990f7bec..93cb1a8d53 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -679,27 +679,9 @@ public struct PresentationResourcesChat { }) } - public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in - return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setStrokeColor(theme.list.itemAccentColor.cgColor) - context.setLineWidth(1.67) - context.setLineCap(.round) - context.setLineJoin(.round) - - context.translateBy(x: 2.0, y: 1.0) - - context.move(to: CGPoint(x: 4.0, y: 0.0)) - context.addLine(to: CGPoint(x: 4.0, y: 10.0)) - context.strokePath() - - context.move(to: CGPoint(x: 0.0, y: 6.0)) - context.addLine(to: CGPoint(x: 4.0, y: 10.0)) - context.addLine(to: CGPoint(x: 8.0, y: 6.0)) - context.strokePath() - }) + public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme, generate: () -> UIImage?) -> UIImage? { + return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { _ in + return generate() }) } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 805a2797d2..722093276f 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -7,7 +7,6 @@ import Intents import Postbox import PushKit import AsyncDisplayKit -import CloudKit import TelegramUIPreferences import TelegramPresentationData import TelegramCallsUI diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 7a552e7a4b..e0f430537a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -922,9 +922,34 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView let displayRange = strongSelf.displayedItemRange if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange { - let firstEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.firstIndex] - - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + var anchorIndex: MessageIndex? + loop: for index in visibleRange.firstIndex ..< filteredEntries.count { + switch filteredEntries[filteredEntries.count - 1 - index] { + case let .MessageEntry(message, _, _, _, _, _): + if message.adAttribute == nil { + anchorIndex = message.index + break loop + } + case let .MessageGroupEntry(_, messages, _): + for (message, _, _, _) in messages { + if message.adAttribute == nil { + anchorIndex = message.index + break loop + } + } + default: + break + } + } + if anchorIndex == nil, let historyView = historyView { + for entry in historyView.originalView.entries { + anchorIndex = entry.message.index + break + } + } + if let anchorIndex = anchorIndex { + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(anchorIndex), anchorIndex: .message(anchorIndex), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + } } else { if let subject = subject, case let .message(messageId, highlight, _) = subject { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index f19252a3f5..786fd31eda 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -81,6 +81,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { var status: Signal { return .single(nil) } + + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + var tabBarOffset: CGFloat { + return 0.0 + } private var disposable: Disposable? @@ -139,7 +144,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop, presentationData) @@ -156,7 +161,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index 202a527575..dad11c6bb0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -24,7 +24,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let listNode: ChatHistoryListNode - private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -54,6 +54,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { var status: Signal { self.statusPromise.get() } + + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + var tabBarOffset: CGFloat { + return 0.0 + } init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) { self.context = context @@ -129,8 +134,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { strongSelf.playlistStateAndType = nil } - if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) + if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) } } }) @@ -180,8 +185,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) var topPanelHeight: CGFloat = 0.0 if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType { @@ -416,11 +421,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { }) } - transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight))) + transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight))) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) + self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight + topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) if isScrollingLockedAtTop { switch self.listNode.visibleContentOffset() { case let .known(value) where value <= CGFloat.ulpOfOne: diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift index 4f150717cd..e5edf5d591 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -124,6 +124,11 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { var status: Signal { return .single(nil) } + + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + var tabBarOffset: CGFloat { + return 0.0 + } private var disposable: Disposable? @@ -183,7 +188,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop) self.presentationDataPromise.set(.single(presentationData)) @@ -200,7 +205,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) } } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 273e2d526b..4a8a76f534 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -18,6 +18,7 @@ import ShimmerEffect import QuartzCore import DirectMediaImageCache import ComponentFlow +import TelegramNotices private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext @@ -634,15 +635,15 @@ private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor { return self.indexValue } - let timestamp: Int32 + let localMonthTimestamp: Int32 override var tag: Int32 { - return self.timestamp + return self.localMonthTimestamp } - init(index: Int, messageId: MessageId, timestamp: Int32) { + init(index: Int, messageId: MessageId, localMonthTimestamp: Int32) { self.indexValue = index self.messageId = messageId - self.timestamp = timestamp + self.localMonthTimestamp = localMonthTimestamp } } @@ -651,38 +652,21 @@ private final class VisualMediaItem: SparseItemGrid.Item { override var index: Int { return self.indexValue } - let timestamp: Int32 - let message: Message? - let isLocal: Bool - - enum StableId: Hashable { - case message(UInt32) - case placeholder(MessageId) - case hole(UInt32) - } - - var stableId: StableId { - if let message = self.message { - return .message(message.stableId) - } else { - preconditionFailure() - //return .placeholder(self.id) - } - } + let localMonthTimestamp: Int32 + let message: Message override var id: AnyHashable { - return AnyHashable(self.stableId) + return AnyHashable(self.message.stableId) } override var tag: Int32 { - return self.timestamp + return self.localMonthTimestamp } - init(index: Int, message: Message, isLocal: Bool) { + init(index: Int, message: Message, localMonthTimestamp: Int32) { self.indexValue = index self.message = message - self.timestamp = message.timestamp - self.isLocal = isLocal + self.localMonthTimestamp = localMonthTimestamp } } @@ -693,11 +677,39 @@ private final class NullActionClass: NSObject, CAAction { private let nullAction = NullActionClass() +private struct Month: Equatable { + var packedValue: Int32 + + init(packedValue: Int32) { + self.packedValue = packedValue + } + + init(localTimestamp: Int32) { + var time: time_t = time_t(localTimestamp) + var timeinfo: tm = tm() + gmtime_r(&time, &timeinfo) + + let year = UInt32(timeinfo.tm_year) + let month = UInt32(timeinfo.tm_mon) + + self.packedValue = Int32(bitPattern: year | (month << 16)) + } + + var year: Int32 { + return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 0) & 0xffff) + } + + var month: Int32 { + return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 16) & 0xffff) + } +} + private final class ItemLayer: CALayer, SparseItemGridLayer { var item: VisualMediaItem? - var shimmerLayer: SparseItemGrid.ShimmerLayer? var disposable: Disposable? + var hasContents: Bool = false + override init() { super.init() @@ -719,26 +731,13 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { func bind(item: VisualMediaItem) { self.item = item - /*if self.contents == nil, let message = item.message { - self.backgroundColor = UIColor(rgb: UInt32(clamping: UInt(bitPattern: String("\(message.id)").hashValue) & 0xffffffff)).cgColor - }*/ - self.updateShimmerLayer() } func updateShimmerLayer() { - if self.contents == nil { - if self.shimmerLayer == nil { - let shimmerLayer = SparseItemGrid.ShimmerLayer() - self.shimmerLayer = shimmerLayer - shimmerLayer.frame = self.bounds - self.addSublayer(shimmerLayer) - } - } else if let shimmerLayer = self.shimmerLayer { - self.shimmerLayer = nil - shimmerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerLayer] _ in - shimmerLayer?.removeFromSuperlayer() - }) + if self.hasContents { + self.removeAnimation(forKey: "shimmer") + self.contentsRect = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) } } @@ -746,73 +745,210 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { self.item = nil } - func update(size: CGSize) { - if let shimmerLayer = self.shimmerLayer { - shimmerLayer.frame = CGRect(origin: CGPoint(), size: size) - } - /*var dimensions: CGSize? + func needsShimmer() -> Bool { + return !self.hasContents + } - if let item = self.item, let message = item.message { - for media in message.media { - if let image = media as? TelegramMediaImage, let representation = image.representations.last { - dimensions = representation.dimensions.cgSize - } else if let file = media as? TelegramMediaFile { - dimensions = file.dimensions?.cgSize ?? CGSize(width: 640.0, height: 480.0) - } + func update(size: CGSize) { + } +} + +private final class ItemView: UIView, SparseItemGridView { + var item: VisualMediaItem? + var disposable: Disposable? + + var hasContents: Bool = false + + var messageItem: ListMessageItem? + var messageItemNode: ListViewItemNode? + var interaction: ListMessageItemInteraction? + let buttonNode: HighlightTrackingButtonNode + + override init(frame: CGRect) { + self.buttonNode = HighlightTrackingButtonNode() + + super.init(frame: frame) + + self.addSubnode(self.buttonNode) + self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + strongSelf.messageItemNode?.setHighlighted(highlighted, at: CGPoint(), animated: !highlighted) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + } + + @objc func pressed() { + guard let itemNode = self.messageItemNode else { + return + } + + if let item = self.item, let messageItem = self.messageItem, let itemNode = itemNode as? ListMessageFileItemNode { + if case let .selectable(selected) = messageItem.selection { + self.interaction?.toggleMessagesSelection([item.message.id], !selected) + } else { + itemNode.activateMedia() } } + } - if let dimensions = dimensions { - let scaledSize = dimensions.aspectFilled(size) - let scaledRect = CGRect(origin: CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0), size: scaledSize) - self.contentsRect = CGRect(origin: CGPoint(x: scaledRect.minX / size.width, y: scaledRect.minY / size.height), size: CGSize(width: scaledRect.width / size.width, height: scaledRect.height / size.height)) + func bind( + item: VisualMediaItem, + presentationData: ChatPresentationData, + context: AccountContext, + chatLocation: ChatLocation, + interaction: ListMessageItemInteraction, + isSelected: Bool?, + size: CGSize + ) { + self.item = item + self.interaction = interaction + + let messageItem = ListMessageItem( + presentationData: presentationData, + context: context, + chatLocation: chatLocation, + interaction: interaction, + message: item.message, + selection: isSelected.flatMap { isSelected in + return .selectable(selected: isSelected) + } ?? .none, + displayHeader: false + ) + self.messageItem = messageItem + + let messageItemNode: ListViewItemNode + if let current = self.messageItemNode { + messageItemNode = current + messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2), completion: { layout, apply in + current.contentSize = layout.contentSize + current.insets = layout.insets + + apply(ListViewItemApply(isOnScreen: true)) + }) } else { - self.contentsRect = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) - }*/ + var itemNode: ListViewItemNode? + messageItem.nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + messageItemNode = itemNode! + self.messageItemNode = messageItemNode + self.buttonNode.addSubnode(messageItemNode) + } + + messageItemNode.frame = CGRect(origin: CGPoint(), size: size) + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + } + func unbind() { + self.item = nil + } + + func needsShimmer() -> Bool { + return false + } + + func update(size: CGSize) { } } private final class SparseItemGridBindingImpl: SparseItemGridBinding { - private let context: AccountContext - private let directMediaImageCache: DirectMediaImageCache - private let strings: PresentationStrings + let context: AccountContext + let chatLocation: ChatLocation + let directMediaImageCache: DirectMediaImageCache + let strings: PresentationStrings + let useListItems: Bool + let listItemInteraction: ListMessageItemInteraction + let chatControllerInteraction: ChatControllerInteraction + let chatPresentationData: ChatPresentationData var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal)? var onTapImpl: ((VisualMediaItem) -> Void)? var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? + var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? + var onBeginFastScrollingImpl: (() -> Void)? + var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? - init(context: AccountContext, directMediaImageCache: DirectMediaImageCache) { + init(context: AccountContext, chatLocation: ChatLocation, useListItems: Bool, listItemInteraction: ListMessageItemInteraction, chatControllerInteraction: ChatControllerInteraction, directMediaImageCache: DirectMediaImageCache) { self.context = context + self.chatLocation = chatLocation + self.useListItems = useListItems + self.listItemInteraction = listItemInteraction + self.chatControllerInteraction = chatControllerInteraction self.directMediaImageCache = directMediaImageCache - self.strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.strings = presentationData.strings + + let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) + self.chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0) } - func createLayer() -> SparseItemGridLayer { + func createLayer() -> SparseItemGridLayer? { + if self.useListItems { + return nil + } return ItemLayer() } - func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer]) { + func createView() -> SparseItemGridView? { + if !self.useListItems { + return nil + } + return ItemView() + } + + func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], synchronous: Bool) { for i in 0 ..< items.count { - guard let item = items[i] as? VisualMediaItem, let layer = layers[i] as? ItemLayer else { - continue - } - if layer.bounds.isEmpty { + guard let item = items[i] as? VisualMediaItem else { continue } - let imageWidthSpec: Int - if layer.bounds.width <= 50 { - imageWidthSpec = 64 - } else if layer.bounds.width <= 100 { - imageWidthSpec = 150 - } else if layer.bounds.width <= 140 { - imageWidthSpec = 200 + if self.useListItems { + guard let view = layers[i].view as? ItemView else { + continue + } + view.bind( + item: item, + presentationData: chatPresentationData, + context: self.context, + chatLocation: self.chatLocation, + interaction: self.listItemInteraction, + isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), + size: view.bounds.size + ) } else { - imageWidthSpec = 280 - } + guard let layer = layers[i].layer as? ItemLayer else { + continue + } + if layer.bounds.isEmpty { + continue + } + + let imageWidthSpec: Int + if layer.bounds.width <= 50 { + imageWidthSpec = 64 + } else if layer.bounds.width <= 100 { + imageWidthSpec = 150 + } else if layer.bounds.width <= 140 { + imageWidthSpec = 200 + } else { + imageWidthSpec = 280 + } + + let message = item.message - if let message = item.message { var selectedMedia: Media? for media in message.media { if let image = media as? TelegramMediaImage { @@ -825,22 +961,38 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } if let selectedMedia = selectedMedia { if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec) { - layer.contents = result.image?.cgImage + if let image = result.image { + layer.contents = image.cgImage + layer.hasContents = true + if !synchronous { + layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } if let loadSignal = result.loadSignal { layer.disposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak layer] image in guard let layer = layer else { return } + let copyLayer = ItemLayer() + copyLayer.contents = layer.contents + copyLayer.contentsRect = layer.contentsRect + copyLayer.frame = layer.bounds + layer.addSublayer(copyLayer) + copyLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyLayer] _ in + copyLayer?.removeFromSuperlayer() + }) + layer.contents = image?.cgImage + layer.hasContents = true layer.updateShimmerLayer() }) } } } - } - layer.bind(item: item) + layer.bind(item: item) + } } } @@ -852,8 +1004,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } func scrollerTextForTag(tag: Int32) -> String? { - let (year, month) = listMessageDateHeaderInfo(timestamp: tag) - return stringForMonth(strings: self.strings, month: month, ofYear: year) + let month = Month(packedValue: tag) + return stringForMonth(strings: self.strings, month: month.month, ofYear: month.year) } func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { @@ -878,96 +1030,23 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { func didScroll() { self.didScrollImpl?() } -} -/*private struct VisualMediaItemCollection { - var items: [VisualMediaItem] - var totalCount: Int - - func item(at index: Int) -> VisualMediaItem? { - func binarySearch(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? { - var lowerIndex = 0 - var upperIndex = inputArr.count - 1 - - if lowerIndex > upperIndex { - return nil - } - - while true { - let currentIndex = (lowerIndex + upperIndex) / 2 - let value = extract(inputArr[currentIndex]) - - if value == searchItem { - return currentIndex - } else if lowerIndex > upperIndex { - return nil - } else { - if (value > searchItem) { - upperIndex = currentIndex - 1 - } else { - lowerIndex = currentIndex + 1 - } - } - } - } - - if let itemIndex = binarySearch(self.items, extract: \.index, searchItem: index) { - return self.items[itemIndex] - } - return nil + func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { + self.coveringInsetOffsetUpdatedImpl?(transition) } - func closestHole(at index: Int) -> (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)? { - var minDistance: Int? - for i in 0 ..< self.items.count { - if self.items[i].isLocal { - continue - } - if let minDistanceValue = minDistance { - if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) { - minDistance = i - } - } else { - minDistance = i - } - } - if let minDistance = minDistance { - let distance = index - self.items[minDistance].index - if abs(distance) <= 2 { - return (self.items[minDistance].id, .around) - } else if distance < 0 { - return (self.items[minDistance].id, .earlier) - } else { - return (self.items[minDistance].id, .later) - } - } - return nil + func onBeginFastScrolling() { + self.onBeginFastScrollingImpl?() } - func closestItem(at index: Int) -> VisualMediaItem? { - if let item = self.item(at: index) { - return item - } - var minDistance: Int? - for i in 0 ..< self.items.count { - if self.items[i].isLocal { - continue - } - if let minDistanceValue = minDistance { - if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) { - minDistance = i - } - } else { - minDistance = i - } - } - if let minDistance = minDistance { - return self.items[minDistance] + func getShimmerColors() -> SparseItemGrid.ShimmerColors { + if let getShimmerColorsImpl = self.getShimmerColorsImpl { + return getShimmerColorsImpl() } else { - return nil + return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } } -}*/ +} private func tagMaskForType(_ type: PeerInfoVisualMediaPaneNode.ContentType) -> MessageTags { switch type { @@ -979,97 +1058,24 @@ private func tagMaskForType(_ type: PeerInfoVisualMediaPaneNode.ContentType) -> return .video case .gifs: return .gif + case .files: + return .file + case .voiceAndVideoMessages: + return .voiceOrInstantVideo + case .music: + return .music } } -/*private enum ItemsLayout { - final class Grid { - let containerWidth: CGFloat - let itemCount: Int - let itemSpacing: CGFloat - let itemsInRow: Int - let itemSize: CGFloat - let rowCount: Int - let contentHeight: CGFloat - - init(containerWidth: CGFloat, zoomLevel: PeerInfoVisualMediaPaneNode.ZoomLevel, itemCount: Int, bottomInset: CGFloat) { - self.containerWidth = containerWidth - self.itemCount = itemCount - self.itemSpacing = 1.0 - let minItemsInRow: Int - let maxItemsInRow: Int - switch zoomLevel { - case .level2: - minItemsInRow = 2 - maxItemsInRow = 4 - case .level3: - minItemsInRow = 3 - maxItemsInRow = 6 - case .level4: - minItemsInRow = 4 - maxItemsInRow = 8 - case .level5: - minItemsInRow = 5 - maxItemsInRow = 10 - } - self.itemsInRow = max(minItemsInRow, min(maxItemsInRow, Int(containerWidth / 140.0))) - self.itemSize = floor(containerWidth / CGFloat(itemsInRow)) - - self.rowCount = itemCount / self.itemsInRow + (itemCount % self.itemsInRow == 0 ? 0 : 1) - - self.contentHeight = CGFloat(self.rowCount + 1) * self.itemSpacing + CGFloat(rowCount) * itemSize + bottomInset - } - - func visibleRange(rect: CGRect) -> (Int, Int) { - var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing))) - minVisibleRow = max(0, minVisibleRow) - var maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing))) - maxVisibleRow = min(self.rowCount - 1, maxVisibleRow) - - let minVisibleIndex = minVisibleRow * itemsInRow - let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * itemsInRow - 1) - - return (minVisibleIndex, maxVisibleIndex) - } - - func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect { - let rowIndex = index / Int(self.itemsInRow) - let columnIndex = index % Int(self.itemsInRow) - let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (self.itemSize + self.itemSpacing), y: self.itemSpacing + CGFloat(rowIndex) * (self.itemSize + self.itemSpacing)) - return CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == self.itemsInRow ? (self.containerWidth - itemOrigin.x) : self.itemSize, height: self.itemSize)) - } - } - - case grid(Grid) - - var contentHeight: CGFloat { - switch self { - case let .grid(grid): - return grid.contentHeight - } - } - - func visibleRange(rect: CGRect) -> (Int, Int) { - switch self { - case let .grid(grid): - return grid.visibleRange(rect: rect) - } - } - - func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect { - switch self { - case let .grid(grid): - return grid.frame(forItemAt: index, sideInset: sideInset) - } - } -}*/ - final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { enum ContentType { case photoOrVideo case photo case video case gifs + case files + case voiceAndVideoMessages + case music } struct ZoomLevel { @@ -1088,8 +1094,6 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro weak var parentController: ViewController? - private let scrollingArea: SparseItemGridScrollingArea - //private let scrollNode: ASScrollNode private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl private let directMediaImageCache: DirectMediaImageCache @@ -1102,7 +1106,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro return self._itemInteraction! } - private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -1114,15 +1118,16 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro var status: Signal { self.statusPromise.get() } + + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + var tabBarOffset: CGFloat { + return self.itemGrid.coveringInsetOffset + } private let listDisposable = MetaDisposable() private var hiddenMediaDisposable: Disposable? - //private var mediaItems = VisualMediaItemCollection(items: [], totalCount: 0) - //private var itemsLayout: ItemsLayout? - //private var visibleMediaItems: [VisualMediaItem.StableId: VisualMediaItemNode] = [:] private var numberOfItemsToRequest: Int = 50 - //private var currentView: MessageHistoryView? private var isRequestingView: Bool = false private var isFirstHistoryView: Bool = true @@ -1131,7 +1136,6 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private var animationTimer: SwiftSignalKit.Timer? private var listSource: SparseMessageList - private var requestedPlaceholderIds = Set() var openCurrentDate: (() -> Void)? var paneDidScroll: (() -> Void)? @@ -1143,16 +1147,70 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.contentType = contentType self.contentTypePromise = ValuePromise(contentType) - self.scrollingArea = SparseItemGridScrollingArea() - //self.scrollNode = ASScrollNode() self.itemGrid = SparseItemGrid() self.directMediaImageCache = DirectMediaImageCache(account: context.account) - self.itemGridBinding = SparseItemGridBindingImpl(context: context, directMediaImageCache: self.directMediaImageCache) + + let useListItems: Bool + switch contentType { + case .files, .voiceAndVideoMessages, .music: + useListItems = true + default: + useListItems = false + } + + let listItemInteraction = ListMessageItemInteraction( + openMessage: { message, mode in + return chatControllerInteraction.openMessage(message, mode) + }, + openMessageContextMenu: { message, bool, node, rect, gesture in + chatControllerInteraction.openMessageContextMenu(message, bool, node, rect, gesture) + }, + toggleMessagesSelection: { messageId, selected in + chatControllerInteraction.toggleMessagesSelection(messageId, selected) + }, + openUrl: { url, param1, param2, message in + chatControllerInteraction.openUrl(url, param1, param2, message) + }, + openInstantPage: { message, data in + chatControllerInteraction.openInstantPage(message, data) + }, + longTap: { action, message in + chatControllerInteraction.longTap(action, message) + }, + getHiddenMedia: { + return chatControllerInteraction.hiddenMedia + } + ) + + self.itemGridBinding = SparseItemGridBindingImpl( + context: context, + chatLocation: .peer(peerId), + useListItems: useListItems, + listItemInteraction: listItemInteraction, + chatControllerInteraction: chatControllerInteraction, + directMediaImageCache: self.directMediaImageCache + ) self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType)) super.init() + let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let strongSelf = self else { + return + } + if count < 1 { + //TODO:localize + strongSelf.itemGrid.updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip(animation: "anim_infotip", text: "You can hold and move this bar for faster scrolling", completed: { + guard let strongSelf = self else { + return + } + let _ = ApplicationSpecificNotice.incrementSharedMediaScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager, count: 1).start() + })) + } + }) + self.itemGridBinding.loadHoleImpl = { [weak self] hole, location in guard let strongSelf = self else { return .never() @@ -1164,10 +1222,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro guard let strongSelf = self else { return } - guard let message = item.message else { - return - } - let _ = strongSelf.chatControllerInteraction.openMessage(message, .default) + let _ = strongSelf.chatControllerInteraction.openMessage(item.message, .default) } self.itemGridBinding.onTagTapImpl = { [weak self] in @@ -1182,21 +1237,64 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro return } strongSelf.paneDidScroll?() + + strongSelf.cancelPreviewGestures() } - /*self.scrollingArea.beginScrolling = { [weak self] in - guard let strongSelf = self else { - return nil - } - return strongSelf.scrollNode.view - } - - self.scrollingArea.openCurrentDate = { [weak self] in + self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in guard let strongSelf = self else { return } - strongSelf.openCurrentDate?() - }*/ + strongSelf.tabBarOffsetUpdated?(transition) + } + + var processedOnBeginFastScrolling = false + self.itemGridBinding.onBeginFastScrollingImpl = { [weak self] in + guard let strongSelf = self else { + return + } + if processedOnBeginFastScrolling { + return + } + processedOnBeginFastScrolling = true + + let _ = (ApplicationSpecificNotice.getSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { count in + guard let strongSelf = self else { + return + } + if count < 1 || true { + let _ = ApplicationSpecificNotice.incrementSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager).start() + + var currentNode: ASDisplayNode = strongSelf + var result: PeerInfoScreenNode? + while true { + if let currentNode = currentNode as? PeerInfoScreenNode { + result = currentNode + break + } else if let supernode = currentNode.supernode { + currentNode = supernode + } else { + break + } + } + if let result = result { + result.displaySharedMediaFastScrollingTooltip() + } + } + }) + } + + self.itemGridBinding.getShimmerColorsImpl = { [weak self] in + guard let strongSelf = self, let presentationData = strongSelf.currentParams?.presentationData else { + return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) + } + + let backgroundColor = presentationData.theme.list.mediaPlaceholderColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } self._itemInteraction = VisualMediaItemInteraction( openMessage: { [weak self] message in @@ -1210,18 +1308,6 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } ) self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } - - /*self.scrollNode.view.delaysContentTouches = false - self.scrollNode.view.canCancelContentTouches = true - self.scrollNode.view.showsVerticalScrollIndicator = false - if #available(iOS 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never - } - self.scrollNode.view.scrollsToTop = false - self.scrollNode.view.delegate = self - - self.addSubnode(self.scrollNode) - self.addSubnode(self.scrollingArea)*/ self.addSubnode(self.itemGrid) @@ -1266,6 +1352,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro summaries.append(.video) case .gifs: summaries.append(.gif) + case .files: + summaries.append(.file) + case .voiceAndVideoMessages: + summaries.append(.voiceOrInstantVideo) + case .music: + summaries.append(.music) } return context.account.postbox.combinedView(keys: summaries.map { tag in return PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud) @@ -1281,6 +1373,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro summaries.append(.video) case .gifs: summaries.append(.gif) + case .files: + summaries.append(.file) + case .voiceAndVideoMessages: + summaries.append(.voiceOrInstantVideo) + case .music: + summaries.append(.music) } var result: [MessageTags: Int32] = [:] for tag in summaries { @@ -1345,6 +1443,33 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } else { return nil } + case .files: + let fileCount: Int32 = dict[.file] ?? 0 + + //TODO:localize + if fileCount != 0 { + return PeerInfoStatusData(text: "\(fileCount) files", isActivity: false) + } else { + return nil + } + case .voiceAndVideoMessages: + let itemCount: Int32 = dict[.voiceOrInstantVideo] ?? 0 + + //TODO:localize + if itemCount != 0 { + return PeerInfoStatusData(text: "\(itemCount) voice messages", isActivity: false) + } else { + return nil + } + case .music: + let itemCount: Int32 = dict[.music] ?? 0 + + //TODO:localize + if itemCount != 0 { + return PeerInfoStatusData(text: "\(itemCount) music files", isActivity: false) + } else { + return nil + } } })) } @@ -1392,83 +1517,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro func updateZoomLevel(level: ZoomLevel) { self.itemGrid.setZoomLevel(level: level.value) - - /*if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { - - var currentTopVisibleItemFrame: CGRect? - if let itemsLayout = self.itemsLayout { - let headerItemMinY = self.scrollNode.view.bounds.minY + 1.0 - let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: self.scrollNode.view.bounds) - - if minVisibleIndex <= maxVisibleIndex { - for i in minVisibleIndex ... maxVisibleIndex { - let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset) - - if currentTopVisibleItemFrame == nil && itemFrame.maxY > headerItemMinY { - currentTopVisibleItemFrame = self.scrollNode.view.convert(itemFrame, to: self.view) - break - } - } - } - } - - self.itemsLayout = nil - - let copyView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) - - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) - - var updatedTopVisibleItemFrame: CGRect? - if let itemsLayout = self.itemsLayout { - let headerItemMinY = self.scrollNode.view.bounds.minY + 1.0 - let (updatedMinVisibleIndex, updatedMaxVisibleIndex) = itemsLayout.visibleRange(rect: self.scrollNode.view.bounds) - - if updatedMinVisibleIndex <= updatedMaxVisibleIndex { - for i in updatedMinVisibleIndex ... updatedMaxVisibleIndex { - let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset) - - if updatedTopVisibleItemFrame == nil && itemFrame.maxY > headerItemMinY { - updatedTopVisibleItemFrame = self.scrollNode.view.convert(itemFrame, to: self.view) - break - } - } - } - } - - if let copyView = copyView, let currentTopVisibleItemFrame = currentTopVisibleItemFrame, let updatedTopVisibleItemFrame = updatedTopVisibleItemFrame { - self.view.addSubview(copyView) - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in - copyView?.removeFromSuperview() - }) - - let additionalOffset = CGPoint(x: updatedTopVisibleItemFrame.minX - currentTopVisibleItemFrame.minX, y: updatedTopVisibleItemFrame.minY - currentTopVisibleItemFrame.minY) - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: self.scrollNode.view.contentOffset.y + additionalOffset.y), animated: false) - - let widthFactor = updatedTopVisibleItemFrame.width / currentTopVisibleItemFrame.width - copyView.layer.animateScale(from: 1.0, to: widthFactor, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in }) - let copyOffset = CGPoint(x: -self.scrollNode.bounds.width * (1.0 - widthFactor) * 0.5, y: -self.scrollNode.bounds.height * (1.0 - widthFactor) * 0.5)//.offsetBy(dx: additionalOffset.x, dy: additionalOffset.y) - copyView.layer.animatePosition(from: CGPoint(), to: copyOffset, duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - - self.scrollNode.layer.animateScale(from: 1.0 / widthFactor, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true) - let originalOffset = CGPoint(x: -self.scrollNode.bounds.width * (1.0 - 1.0 / widthFactor) * 0.5, y: -self.scrollNode.bounds.height * (1.0 - 1.0 / widthFactor) * 0.5)//.offsetBy(dx: additionalOffset.x, dy: additionalOffset.y) - self.scrollNode.layer.animatePosition(from: originalOffset, to: CGPoint(), duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true, additive: true) - } - }*/ } func ensureMessageIsVisible(id: MessageId) { - /*let activeRect = self.scrollNode.bounds - for item in self.mediaItems.items { - if let message = item.message, message.id == id { - if let itemNode = self.visibleMediaItems[item.stableId] { - if !activeRect.contains(itemNode.frame) { - let targetContentOffset = CGPoint(x: 0.0, y: max(-self.scrollNode.view.contentInset.top, itemNode.frame.minY - (self.scrollNode.frame.height - itemNode.frame.height) / 2.0)) - self.scrollNode.view.setContentOffset(targetContentOffset, animated: false) - } - } - break - } - }*/ } private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) { @@ -1491,14 +1542,16 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } private func updateHistory(list: SparseMessageList.State, synchronous: Bool, reloadAtTop: Bool) { + let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) + var mappedItems: [SparseItemGrid.Item] = [] var mappeHoles: [SparseItemGrid.HoleAnchor] = [] for item in list.items { switch item.content { - case let .message(message, isLocal): - mappedItems.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal)) + case let .message(message, _): + mappedItems.append(VisualMediaItem(index: item.index, message: message, localMonthTimestamp: Month(localTimestamp: message.timestamp + timezoneOffset).packedValue)) case let .placeholder(id, timestamp): - mappeHoles.append(VisualMediaHoleAnchor(index: item.index, messageId: id, timestamp: timestamp)) + mappeHoles.append(VisualMediaHoleAnchor(index: item.index, messageId: id, localMonthTimestamp: Month(localTimestamp: timestamp + timezoneOffset).packedValue)) } } @@ -1509,41 +1562,19 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro itemBinding: self.itemGridBinding ) - if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) - } - - /*self.mediaItems = VisualMediaItemCollection(items: [], totalCount: list.totalCount) - for item in list.items { - switch item.content { - case let .message(message, isLocal): - self.mediaItems.items.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal)) - case let .placeholder(id, timestamp): - self.mediaItems.items.append(VisualMediaItem(index: item.index, id: id, timestamp: timestamp)) - } - } - self.itemsLayout = nil - - let wasFirstHistoryView = self.isFirstHistoryView - self.isFirstHistoryView = false - - if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { - if synchronous { - if let copyView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { - copyView.backgroundColor = self.context.sharedContext.currentPresentationData.with({ $0 }).theme.list.plainBackgroundColor - self.view.addSubview(copyView) - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in - copyView?.removeFromSuperview() - }) - } - } - self.ignoreScrolling = true + if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { + var gridSnapshot: UIView? if reloadAtTop { - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView || synchronous, transition: .immediate) - self.ignoreScrolling = false - }*/ + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) + if let gridSnapshot = gridSnapshot { + self.view.addSubview(gridSnapshot) + gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in + gridSnapshot?.removeFromSuperview() + }) + } + } if !self.didSetReady { self.didSetReady = true @@ -1552,13 +1583,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func scrollToTop() -> Bool { - /*if self.scrollNode.view.contentOffset.y > 0.0 { - self.scrollNode.view.setContentOffset(CGPoint(), animated: true) - return true - } else { - return false - }*/ - return false + return self.itemGrid.scrollToTop() } func findLoadedMessage(id: MessageId) -> Message? { @@ -1569,7 +1594,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro guard let item = item as? VisualMediaItem else { continue } - if let message = item.message, message.id == id { + if item.message.id == id { return item.message } } @@ -1577,12 +1602,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func updateHiddenMedia() { - self.itemGrid.forEachVisibleItem { itemLayer in - guard let itemLayer = itemLayer as? ItemLayer else { + self.itemGrid.forEachVisibleItem { item in + guard let itemLayer = item.layer as? ItemLayer else { return } - if let item = itemLayer.item, let message = item.message { - if self.itemInteraction.hiddenMedia[message.id] != nil { + if let item = itemLayer.item { + if self.itemInteraction.hiddenMedia[item.message.id] != nil { itemLayer.isHidden = true } else { itemLayer.isHidden = false @@ -1636,18 +1661,23 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func cancelPreviewGestures() { - /*for (_, itemNode) in self.visibleMediaItems { - itemNode.cancelPreviewGesture() - }*/ + self.itemGrid.forEachVisibleItem { item in + guard let itemView = item.view as? ItemView else { + return + } + if let messageItemNode = itemView.messageItemNode as? ListMessageFileItemNode { + messageItemNode.cancelPreviewGesture() + } + } } func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { var foundItemLayer: SparseItemGridLayer? - self.itemGrid.forEachVisibleItem { itemLayer in - guard let itemLayer = itemLayer as? ItemLayer else { + self.itemGrid.forEachVisibleItem { item in + guard let itemLayer = item.layer as? ItemLayer else { return } - if let item = itemLayer.item, let message = item.message, message.id == messageId { + if let item = itemLayer.item, item.message.id == messageId { foundItemLayer = itemLayer } } @@ -1679,57 +1709,107 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func updateSelectedMessages(animated: Bool) { - /*self.itemInteraction.selectedMessageIds = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds } - for (_, itemNode) in self.visibleMediaItems { - itemNode.updateSelectionState(animated: animated) - }*/ + self.itemGrid.forEachVisibleItem { item in + guard let itemView = item.view as? ItemView else { + return + } + if let item = itemView.item { + itemView.bind( + item: item, + presentationData: self.itemGridBinding.chatPresentationData, + context: self.itemGridBinding.context, + chatLocation: self.itemGridBinding.chatLocation, + interaction: self.itemGridBinding.listItemInteraction, + isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), + size: itemView.bounds.size + ) + } + } } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - //let previousParams = self.currentParams - self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) - transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) if let items = self.items { - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, items: items) - } - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - } + let fixedItemHeight: CGFloat? + switch self.contentType { + case .files, .music, .voiceAndVideoMessages: + let fakeFile = TelegramMediaFile( + fileId: MediaId(namespace: 0, id: 1), + partialReference: nil, + resource: EmptyMediaResource(), + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "image/jpeg", + size: nil, + attributes: [.FileName(fileName: "file")] + ) + let fakeMessage = Message( + stableId: 1, + stableVersion: 1, + id: MessageId(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(1)), namespace: 0, id: 1), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: 1, flags: [], + tags: [], + globalTags: [], + localTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [fakeFile], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [] + ) + let messageItem = ListMessageItem( + presentationData: self.itemGridBinding.chatPresentationData, + context: self.itemGridBinding.context, + chatLocation: self.itemGridBinding.chatLocation, + interaction: self.itemGridBinding.listItemInteraction, + message: fakeMessage, + selection: .none, + displayHeader: false + ) - private var previousDidScrollTimestamp: Double = 0.0 - private var ignoreScrolling: Bool = false - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if self.ignoreScrolling { - return - } - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - } + var itemNode: ListViewItemNode? + messageItem.nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) - private func updateScrollingArea(transition: ContainedViewLayoutTransition) { + if let itemNode = itemNode { + fixedItemHeight = itemNode.contentSize.height + } else { + preconditionFailure() + } + default: + fixedItemHeight = nil + } + + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, items: items) + } } func currentTopTimestamp() -> Int32? { var timestamp: Int32? - self.itemGrid.forEachVisibleItem { itemLayer in + self.itemGrid.forEachVisibleItem { item in if timestamp != nil { return } - guard let itemLayer = itemLayer as? ItemLayer else { + guard let itemLayer = item.layer as? ItemLayer else { return } - if let item = itemLayer.item, let message = item.message { + if let item = itemLayer.item { if let timestampValue = timestamp { - timestamp = max(timestampValue, message.timestamp) + timestamp = max(timestampValue, item.message.timestamp) } else { - timestamp = message.timestamp + timestamp = item.message.timestamp } } } @@ -1740,10 +1820,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro if let items = self.items, !items.items.isEmpty { var previousIndex: Int? for item in items.items { - guard let item = item as? VisualMediaItem, let message = item.message else { + guard let item = item as? VisualMediaItem else { continue } - if message.timestamp <= timestamp { + if item.message.timestamp <= timestamp { break } previousIndex = item.index @@ -1755,102 +1835,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.itemGrid.scrollToItem(at: index) } } - /*guard let currentParams = self.currentParams else { - return - } - guard let itemsLayout = self.itemsLayout else { - return - } - for item in self.mediaItems.items { - if item.timestamp <= timestamp { - let frame = itemsLayout.frame(forItemAt: item.index, sideInset: currentParams.sideInset) - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: frame.minY), animated: false) - - break - } - }*/ } - /*private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) { - guard let itemsLayout = self.itemsLayout else { - return - } - - let activeRect = self.scrollNode.view.bounds - let visibleRect = activeRect.insetBy(dx: 0.0, dy: -400.0) - - let (minActuallyVisibleIndex, maxActuallyVisibleIndex) = itemsLayout.visibleRange(rect: activeRect) - let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect) - - var requestHole: (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)? - - var validIds = Set() - if minVisibleIndex <= maxVisibleIndex { - for itemIndex in minVisibleIndex ... maxVisibleIndex { - let maybeItem = self.mediaItems.item(at: itemIndex) - var findHole = false - if let item = maybeItem { - if item.message == nil { - findHole = true - } - } else { - findHole = true - } - if findHole { - if let hole = self.mediaItems.closestHole(at: itemIndex) { - if requestHole == nil { - requestHole = hole - } - } - } - - let stableId: VisualMediaItem.StableId - if let item = maybeItem { - stableId = item.stableId - } else { - stableId = .hole(UInt32(itemIndex)) - } - - validIds.insert(stableId) - - let itemFrame = itemsLayout.frame(forItemAt: itemIndex, sideInset: sideInset) - - let itemNode: VisualMediaItemNode - if let current = self.visibleMediaItems[stableId] { - itemNode = current - } else { - itemNode = VisualMediaItemNode(context: self.context, interaction: self.itemInteraction) - self.visibleMediaItems[stableId] = itemNode - self.scrollNode.addSubnode(itemNode) - } - itemNode.frame = itemFrame - itemNode.updateAbsoluteRect(itemFrame.offsetBy(dx: 0.0, dy: -activeRect.origin.y), within: activeRect.size) - - var itemSynchronousLoad = false - if itemIndex >= minActuallyVisibleIndex && itemIndex <= maxActuallyVisibleIndex { - itemSynchronousLoad = synchronousLoad - } - itemNode.update(size: itemFrame.size, item: maybeItem, theme: theme, synchronousLoad: itemSynchronousLoad) - itemNode.updateIsVisible(itemFrame.intersects(activeRect)) - } - } - var removeKeys: [VisualMediaItem.StableId] = [] - for (id, _) in self.visibleMediaItems { - if !validIds.contains(id) { - removeKeys.append(id) - } - } - for id in removeKeys { - if let itemNode = self.visibleMediaItems.removeValue(forKey: id) { - itemNode.removeFromSupernode() - } - } - - if let requestHole = requestHole { - self.listSource.loadHole(anchor: requestHole.anchor, direction: requestHole.direction) - } - }*/ - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index e498e843e5..309eb36879 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -1044,7 +1044,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable { } final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { - private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] + private(set) var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = [] @@ -1666,6 +1666,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode + let panelSubtitleNode: MultiScaleTextNode let usernameNodeContainer: ASDisplayNode let usernameNodeRawContainer: ASDisplayNode let usernameNode: MultiScaleTextNode @@ -1720,6 +1721,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.subtitleNodeRawContainer = ASDisplayNode() self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.subtitleNode.displaysAsynchronously = false + + self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) + self.panelSubtitleNode.displaysAsynchronously = false self.usernameNodeContainer = ASDisplayNode() self.usernameNodeRawContainer = ASDisplayNode() @@ -1770,6 +1774,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleNodeContainer.addSubnode(self.titleNode) self.regularContentNode.addSubnode(self.titleNodeContainer) self.subtitleNodeContainer.addSubnode(self.subtitleNode) + self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode) self.regularContentNode.addSubnode(self.subtitleNodeContainer) self.regularContentNode.addSubnode(self.subtitleNodeRawContainer) self.usernameNodeContainer.addSubnode(self.usernameNode) @@ -1899,7 +1904,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } var initializedCredibilityIcon = false - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: PeerInfoStatusData?, isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.state = state self.peer = peer self.avatarListNode.listContainerNode.peer = peer @@ -2019,6 +2024,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var isVerified = false let titleString: NSAttributedString let subtitleString: NSAttributedString + var panelSubtitleString: NSAttributedString? let usernameString: NSAttributedString if let peer = peer, peer.isVerified { isVerified = true @@ -2063,6 +2069,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { } subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + + if let panelStatusData = panelStatusData { + let subtitleColor: UIColor + if panelStatusData.isActivity { + subtitleColor = presentationData.theme.list.itemAccentColor + } else { + subtitleColor = presentationData.theme.list.itemSecondaryTextColor + } + panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(15.0), textColor: subtitleColor) + } } else { subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) @@ -2089,6 +2105,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { TitleNodeStateExpanded: MultiScaleTextState(attributedText: subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height)) ], mainState: TitleNodeStateRegular) self.subtitleNode.accessibilityLabel = subtitleString.string + + let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(states: [ + TitleNodeStateRegular: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height)) + ], mainState: TitleNodeStateRegular) + self.panelSubtitleNode.accessibilityLabel = (panelSubtitleString ?? subtitleString).string let usernameNodeLayout = self.usernameNode.updateLayout(states: [ TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), @@ -2102,6 +2124,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size + let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size if let image = self.titleCredibilityIconNode.image { @@ -2150,17 +2173,53 @@ final class PeerInfoHeaderNode: ASDisplayNode { let avatarMinScale: CGFloat = 0.7 let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset + + let paneAreaExpansionDistance: CGFloat = 32.0 + let effectiveAreaExpansionFraction: CGFloat + if state.isEditing { + effectiveAreaExpansionFraction = 0.0 + } else if isSettings { + var paneAreaExpansionDelta = (self.frame.maxY - navigationHeight) - contentOffset + paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) + effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance + } else { + var paneAreaExpansionDelta = (paneContainerY - navigationHeight) - contentOffset + paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) + effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance + } self.titleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], transition: transition) - let subtitleAlpha: CGFloat = self.isSettings ? 1.0 - titleCollapseFraction : 1.0 + let subtitleAlpha: CGFloat + var subtitleOffset: CGFloat = 0.0 + let panelSubtitleAlpha: CGFloat + var panelSubtitleOffset: CGFloat = 0.0 + if self.isSettings { + subtitleAlpha = 1.0 - titleCollapseFraction + panelSubtitleAlpha = 0.0 + } else { + if (panelSubtitleString ?? subtitleString).string != subtitleString.string { + subtitleAlpha = 1.0 - effectiveAreaExpansionFraction + panelSubtitleAlpha = effectiveAreaExpansionFraction + subtitleOffset = -effectiveAreaExpansionFraction * 5.0 + panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0 + } else { + subtitleAlpha = 1.0 + panelSubtitleAlpha = 0.0 + } + } self.subtitleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: subtitleAlpha, transition: transition) + + self.panelSubtitleNode.update(stateFractions: [ + TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, + TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 + ], alpha: panelSubtitleAlpha, transition: transition) self.usernameNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, @@ -2315,7 +2374,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size) self.subtitleNodeRawContainer.frame = rawSubtitleFrame transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) - transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) + transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) @@ -2353,7 +2413,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { usernameCenter.x = rawTitleFrame.center.x + (usernameCenter.x - rawTitleFrame.center.x) * subtitleScale transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) } - transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) + transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 3a72efe7b8..e68410ea4b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -15,8 +15,10 @@ protocol PeerInfoPaneNode: ASDisplayNode { var parentController: ViewController? { get set } var status: Signal { get } + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } + var tabBarOffset: CGFloat { get } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) func scrollToTop() -> Bool func transferVelocity(_ velocity: CGFloat) func cancelPreviewGestures() @@ -32,21 +34,21 @@ final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey let node: PeerInfoPaneNode var isAnimatingOut: Bool = false - private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)? + private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)? init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { self.key = key self.node = node } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - if let (currentSize, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { - if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { + if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { return } } - self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) - self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition) + self.appliedParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition) } } @@ -401,13 +403,19 @@ private final class PeerInfoPendingPane { paneDidScroll() } case .files: - paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .files) + paneNode = visualPaneNode + //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) case .links: paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage) case .voice: - paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .voiceAndVideoMessages) + paneNode = visualPaneNode + //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo) case .music: - paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .music) + paneNode = visualPaneNode + //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music) case .gifs: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs) paneNode = visualPaneNode @@ -446,6 +454,7 @@ private final class PeerInfoPendingPane { final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private let peerId: PeerId + private let isMediaOnly: Bool weak var parentController: ViewController? @@ -469,6 +478,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat return nil } } + + private let currentPaneStatusPromise = Promise(nil) + var currentPaneStatus: Signal { + return self.currentPaneStatusPromise.get() + } private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] @@ -490,10 +504,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat private var currentAvailablePanes: [PeerInfoPaneKey]? private let updatedPresentationData: (initial: PresentationData, signal: Signal)? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId) { + init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, isMediaOnly: Bool) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId + self.isMediaOnly = isMediaOnly self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -531,6 +546,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.currentPaneUpdated?(true) + + strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil)) } } else if strongSelf.pendingSwitchToPaneKey != key { strongSelf.pendingSwitchToPaneKey = key @@ -634,6 +651,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.transitionFraction = 0.0 self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) self.currentPaneUpdated?(false) + + self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) } default: break @@ -675,6 +694,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.currentAvailablePanes = availablePanes let previousCurrentPaneKey = self.currentPaneKey + var updateCurrentPaneStatus = false if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { var nextCandidatePaneKey: PeerInfoPaneKey? @@ -714,16 +734,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.coveringBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - + + let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne + let tabsHeight: CGFloat = 48.0 - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) - self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition) - - transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - - let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) var visiblePaneIndices: [Int] = [] var requiredPendingKeys: [PeerInfoPaneKey] = [] @@ -794,14 +810,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat ) self.pendingPanes[key] = pane pane.pane.node.frame = paneFrame - pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate) + let paneNode = pane.pane.node + pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in + guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else { + return + } + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) + } + } leftScope = true } } for (key, pane) in self.pendingPanes { pane.pane.node.frame = paneFrame - pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) if pane.isReady { self.pendingPanes.removeValue(forKey: key) @@ -818,6 +843,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.pendingSwitchToPaneKey = nil previousPaneKey = self.currentPaneKey self.currentPaneKey = pendingSwitchToPaneKey + updateCurrentPaneStatus = true updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { if updatedCurrentIndex < previousIndex { @@ -834,7 +860,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { var paneWasAdded = false if pane.node.supernode == nil { - self.addSubnode(pane.node) + self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode) paneWasAdded = true } let indexOffset = CGFloat(index - updatedCurrentIndex) @@ -878,13 +904,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat paneCompletion() }) } - pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } } - - //print("currentPanes: \(self.currentPanes.map { $0.0 })") - - transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight))) + + var tabsOffset: CGFloat = 0.0 + if let currentPane = self.currentPane { + tabsOffset = currentPane.node.tabBarOffset + } + tabsOffset = max(0.0, min(tabsHeight, tabsOffset)) + if isScrollingLockedAtTop || self.isMediaOnly { + tabsOffset = 0.0 + } + var tabsAlpha = 1.0 - tabsOffset / tabsHeight + tabsAlpha *= tabsAlpha + transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight))) + transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) + self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition) + + transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) + self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String switch key { @@ -911,7 +953,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) - pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) } if !self.didSetIsReady && data != nil { if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { @@ -925,5 +967,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { self.currentPaneUpdated?(true) } + if updateCurrentPaneStatus { + self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) + } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f9860132e9..e198026083 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -61,6 +61,7 @@ import TelegramCallsUI import PeerInfoAvatarListNode import PasswordSetupUI import CalendarMessageScreen +import TooltipUI protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -1617,7 +1618,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.scrollNode.canCancelAllTouchesInViews = true self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isSettings: isSettings) - self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId) + self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, isMediaOnly: self.isMediaOnly) super.init() @@ -2267,12 +2268,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return } - if let pane = strongSelf.paneContainerNode.currentPane?.node { - strongSelf.customStatusPromise.set(pane.status) - } else { - strongSelf.customStatusPromise.set(.single(nil)) - } - if let (layout, navigationHeight) = strongSelf.validLayout { if strongSelf.headerNode.isAvatarExpanded { let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) @@ -2291,6 +2286,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } } + + self.customStatusPromise.set(self.paneContainerNode.currentPaneStatus) self.paneContainerNode.requestExpandTabs = { [weak self] in guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else { @@ -6010,6 +6007,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate private weak var mediaGalleryContextMenu: ContextController? + func displaySharedMediaFastScrollingTooltip() { + guard let buttonNode = self.headerNode.navigationButtonContainer.buttonNodes[.more] else { + return + } + guard let controller = self.controller else { + return + } + let buttonFrame = buttonNode.view.convert(buttonNode.bounds, to: self.view) + //TODO:localize + controller.present(TooltipScreen(account: self.context.account, text: "Tap on this icon for calendar view", style: .default, icon: .none, location: .point(buttonFrame.insetBy(dx: 0.0, dy: 5.0), .top), shouldDismissOnTouch: { point in + return .dismiss(consume: false) + }), in: .current) + } + private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) { guard let controller = self.controller else { return @@ -6027,12 +6038,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if !"".isEmpty { canZoom = false } - /*if isZoomIn { - canZoom = pane?.availableZoomLevels().increment != nil - } else { - canZoom = pane?.availableZoomLevels().decrement != nil - }*/ - return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "ZoomOut", textColor: canZoom ? .primary : .disabled, icon: { theme in + return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "Zoom Out", textColor: canZoom ? .primary : .disabled, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) }, action: canZoom ? { action in guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { @@ -6094,8 +6100,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate updatedContentType = .photo case .video: updatedContentType = .photoOrVideo - case .gifs: - updatedContentType = .gifs + default: + updatedContentType = pane.contentType } pane.updateContentType(contentType: updatedContentType) }))) @@ -6118,8 +6124,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate updatedContentType = .photoOrVideo case .video: updatedContentType = .video - case .gifs: - updatedContentType = .gifs + default: + updatedContentType = pane.contentType } pane.updateContentType(contentType: updatedContentType) }))) @@ -6187,7 +6193,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate var contentHeight: CGFloat = 0.0 - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.customStatusData ?? self.data?.status, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -6436,7 +6442,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let (layout, navigationHeight) = self.validLayout { if !additive { - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.customStatusData ?? self.data?.status, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -7308,7 +7314,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) var topHeight = topNavigationBar.backgroundNode.bounds.height if let (layout, _) = self.screenNode.validLayout { - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, transition: transition, additive: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: nil, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, transition: transition, additive: false) } let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.bounds.height diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 1e9e344ce8..6aa915a490 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -18,6 +18,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var experimentalCompatibility: Bool public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool + public var mockICE: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -34,7 +35,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: false, experimentalCompatibility: false, enableDebugDataDisplay: false, - acceleratedStickers: false + acceleratedStickers: false, + mockICE: false ) } @@ -52,7 +54,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: Bool, experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, - acceleratedStickers: Bool + acceleratedStickers: Bool, + mockICE: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -68,6 +71,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.experimentalCompatibility = experimentalCompatibility self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers + self.mockICE = mockICE } public init(from decoder: Decoder) throws { @@ -87,6 +91,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0 self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 + self.mockICE = (try container.decodeIfPresent(Int32.self, forKey: "mockICE") ?? 0) != 0 } public func encode(to encoder: Encoder) throws { @@ -106,6 +111,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility") try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") + try container.encode((self.mockICE ? 1 : 0) as Int32, forKey: "mockICE") } }