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/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..88b8bee9cc 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]) 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() @@ -355,9 +468,101 @@ public final class SparseItemGrid: ASDisplayNode { } } + @objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.items?.itemBinding.didScroll() + } + @objc func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateVisibleItems(resetScrolling: false, 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) } + 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, 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,39 +709,45 @@ 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 } } @@ -535,8 +756,18 @@ public final class SparseItemGrid: ASDisplayNode { items.itemBinding.bindLayers(items: bindItems, layers: bindLayers) } - 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..445a86954d 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -4,6 +4,250 @@ 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 backgroundNode: NavigationBackgroundNode + private var icon: ComponentHostView? + private let content: ComponentHostView + + init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(white: 0.2, alpha: 0.7)) + self.content = ComponentHostView() + + super.init(frame: CGRect()) + + self.addSubnode(self.backgroundNode) + 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 + } + + transition.setFrame(view: self.backgroundNode.view, frame: contentRect) + self.backgroundNode.update(size: contentRect.size, cornerRadius: 8.0, transition: .immediate) + + 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 @@ -324,6 +568,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 +585,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 +600,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 +663,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 +688,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 +718,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 +737,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { self.updateLineIndicator(transition: transition) } + func feedbackTap() { + self.hapticFeedback.tap() + } + public func update( containerSize: CGSize, containerInsets: UIEdgeInsets, @@ -482,8 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { dateString: String, transition: ContainedViewLayoutTransition ) { + self.containerSize = containerSize + if isScrolling { - self.updateActivityTimer() + self.updateActivityTimer(isScrolling: true) } let indicatorSize = self.dateIndicator.update( @@ -508,7 +778,7 @@ 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)) @@ -539,7 +809,13 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { self.lineIndicator.alpha = 1.0 } + self.updateLineTooltip(containerSize: containerSize) + self.updateLineIndicator(transition: transition) + + if isScrolling { + self.displayTooltipOnFirstScroll() + } } private func updateLineIndicator(transition: ContainedViewLayoutTransition) { @@ -567,7 +843,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 +858,68 @@ 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) + + 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: -4.0, dy: -4.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/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..8d74bcbd53 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]) { 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,35 @@ 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 let loadSignal = result.loadSignal { + let shimmerColor = self.getShimmerColors().background layer.disposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak layer] image in guard let layer = layer else { return } + let copyLayer = ItemLayer() + copyLayer.backgroundColor = UIColor(rgb: shimmerColor).cgColor + 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 +1001,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 +1027,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 +1055,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 +1091,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 +1103,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 +1115,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 +1133,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 +1144,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 +1219,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 +1234,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 +1305,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 +1349,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 +1370,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 +1440,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 +1514,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 +1539,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,42 +1559,10 @@ 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) + if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, 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 reloadAtTop { - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: 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 - }*/ - if !self.didSetReady { self.didSetReady = true self.ready.set(.single(true)) @@ -1552,13 +1570,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 +1581,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 +1589,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 +1648,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 +1696,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 +1807,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 +1822,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..7931f379e3 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] = [] diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 3a72efe7b8..4cffdf9854 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 @@ -714,16 +722,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 +798,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) @@ -834,7 +847,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 +891,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 { + 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 +940,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] { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index c6d879b918..232e371cf9 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 } @@ -5996,6 +5997,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 @@ -6080,8 +6095,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate updatedContentType = .photo case .video: updatedContentType = .photoOrVideo - case .gifs: - updatedContentType = .gifs + default: + updatedContentType = pane.contentType } pane.updateContentType(contentType: updatedContentType) }))) @@ -6104,8 +6119,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate updatedContentType = .photoOrVideo case .video: updatedContentType = .video - case .gifs: - updatedContentType = .gifs + default: + updatedContentType = pane.contentType } pane.updateContentType(contentType: updatedContentType) }))) 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") } }