Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2021-10-22 18:59:52 +04:00
commit 17229c7f3d
31 changed files with 2101 additions and 826 deletions

View File

@ -129,20 +129,4 @@ def generate(build_environment: BuildEnvironment, disable_extensions, disable_pr
xcodeproj_path = '{project}/{target}.xcodeproj'.format(project=project_path, target=app_target)
bazel_build_settings_path = '{}/.tulsi/Scripts/bazel_build_settings.py'.format(xcodeproj_path)
with open(bazel_build_settings_path, 'rb') as bazel_build_settings:
bazel_build_settings_contents = bazel_build_settings.read().decode('utf-8')
bazel_build_settings_contents = bazel_build_settings_contents.replace(
'BUILD_SETTINGS = BazelBuildSettings(',
'import os\nBUILD_SETTINGS = BazelBuildSettings('
)
bazel_build_settings_contents = bazel_build_settings_contents.replace(
'\'--cpu=ios_arm64\'',
'\'--cpu=ios_arm64\'.replace(\'ios_arm64\', \'ios_sim_arm64\' if os.environ.get(\'EFFECTIVE_PLATFORM_NAME\') '
'== \'-iphonesimulator\' else \'ios_arm64\')'
)
with open(bazel_build_settings_path, 'wb') as bazel_build_settings:
bazel_build_settings.write(bazel_build_settings_contents.encode('utf-8'))
call_executable(['open', xcodeproj_path])

@ -1 +1 @@
Subproject commit 8c8f4661dba2bbe8578ae42b8ab7001d27357575
Subproject commit 03c89782e9a15d467c7e036ee36f9adb6bdda910

@ -1 +1 @@
Subproject commit 01d37ab862350cb33cbae25cf6622bf534df264f
Subproject commit ec7dd9ddf4b73dedb02df827b7ab3b2cbb1f2ac0

View File

@ -10,12 +10,12 @@ import TelegramPresentationData
import ComponentFlow
import PhotoResources
private final class MediaPreviewNode: ASDisplayNode {
private final class MediaPreviewView: UIView {
private let context: AccountContext
private let message: EngineMessage
private let media: EngineMedia
private let imageNode: TransformImageNode
private let imageView: TransformImageView
private var requestedImage: Bool = false
private var disposable: Disposable?
@ -25,11 +25,15 @@ private final class MediaPreviewNode: ASDisplayNode {
self.message = message
self.media = media
self.imageNode = TransformImageNode()
self.imageView = TransformImageView()
super.init()
super.init(frame: CGRect())
self.addSubnode(self.imageNode)
self.addSubview(self.imageView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
@ -44,7 +48,7 @@ private final class MediaPreviewNode: ASDisplayNode {
if !self.requestedImage {
self.requestedImage = true
let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads)
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads)
}
}
} else if case let .file(file) = self.media {
@ -53,13 +57,13 @@ private final class MediaPreviewNode: ASDisplayNode {
if !self.requestedImage {
self.requestedImage = true
let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true)
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads)
}
}
}
let makeLayout = self.imageNode.asyncLayout()
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let makeLayout = self.imageView.asyncLayout()
self.imageView.frame = CGRect(origin: CGPoint(), size: size)
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
}
@ -152,6 +156,13 @@ private final class ImageCache: Equatable {
var color: UInt32
}
private struct Text: Hashable {
var fontSize: CGFloat
var isSemibold: Bool
var color: UInt32
var string: String
}
private var items: [AnyHashable: UIImage] = [:]
func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage {
@ -169,6 +180,31 @@ private final class ImageCache: Equatable {
self.items[key] = image
return image
}
func text(fontSize: CGFloat, isSemibold: Bool, color: UIColor, string: String) -> UIImage {
let key = AnyHashable(Text(fontSize: fontSize, isSemibold: isSemibold, color: color.argb, string: string))
if let image = self.items[key] {
return image
}
let font: UIFont
if isSemibold {
font = Font.semibold(fontSize)
} else {
font = Font.regular(fontSize)
}
let attributedString = NSAttributedString(string: string, font: font, textColor: color)
let rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil)
let image = generateImage(CGSize(width: ceil(rect.width), height: ceil(rect.height)), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
attributedString.draw(in: rect)
UIGraphicsPopContext()
})!
self.items[key] = image
return image
}
}
private final class DayComponent: Component {
@ -223,28 +259,28 @@ private final class DayComponent: Component {
}
final class View: UIView {
private let buttonNode: HighlightableButtonNode
private let button: HighlightableButton
private let highlightNode: ASImageNode
private let titleNode: ImmediateTextNode
private var mediaPreviewNode: MediaPreviewNode?
private let highlightView: UIImageView
private let titleView: UIImageView
private var mediaPreviewView: MediaPreviewView?
private var action: (() -> Void)?
private var currentMedia: DayMedia?
init() {
self.buttonNode = HighlightableButtonNode()
self.highlightNode = ASImageNode()
self.titleNode = ImmediateTextNode()
self.button = HighlightableButton()
self.highlightView = UIImageView()
self.titleView = UIImageView()
super.init(frame: CGRect())
self.buttonNode.addSubnode(self.highlightNode)
self.buttonNode.addSubnode(self.titleNode)
self.button.addSubview(self.highlightView)
self.button.addSubview(self.titleView)
self.addSubnode(self.buttonNode)
self.addSubview(self.button)
self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
@ -258,58 +294,64 @@ private final class DayComponent: Component {
func update(component: DayComponent, availableSize: CGSize, environment: Environment<ImageCache>, transition: Transition) -> CGSize {
self.action = component.action
let shadowInset: CGFloat = 0.0
let diameter = min(availableSize.width, availableSize.height)
let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))
let imageCache = environment[ImageCache.self]
if component.media != nil {
self.highlightNode.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2))
self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2))
} else if component.isCurrent {
self.highlightNode.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor)
self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor)
} else {
self.highlightNode.image = nil
self.highlightView.image = nil
}
if self.currentMedia != component.media {
self.currentMedia = component.media
if let mediaPreviewNode = self.mediaPreviewNode {
self.mediaPreviewNode = nil
mediaPreviewNode.removeFromSupernode()
if let mediaPreviewView = self.mediaPreviewView {
self.mediaPreviewView = nil
mediaPreviewView.removeFromSuperview()
}
if let media = component.media {
let mediaPreviewNode = MediaPreviewNode(context: component.context, message: media.message, media: media.media)
self.mediaPreviewNode = mediaPreviewNode
self.buttonNode.insertSubnode(mediaPreviewNode, belowSubnode: self.highlightNode)
let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media)
self.mediaPreviewView = mediaPreviewView
self.button.insertSubview(mediaPreviewView, belowSubview: self.highlightView)
}
}
let titleColor: UIColor
let titleFont: UIFont
let titleFontSize: CGFloat
let titleFontIsSemibold: Bool
if component.isCurrent || component.media != nil {
titleColor = component.theme.list.itemCheckColors.foregroundColor
titleFont = Font.semibold(17.0)
titleFontSize = 17.0
titleFontIsSemibold = true
} else if component.isEnabled {
titleColor = component.theme.list.itemPrimaryTextColor
titleFont = Font.regular(17.0)
titleFontSize = 17.0
titleFontIsSemibold = false
} else {
titleColor = component.theme.list.itemDisabledTextColor
titleFont = Font.regular(17.0)
titleFontSize = 17.0
titleFontIsSemibold = false
}
self.titleNode.attributedText = NSAttributedString(string: component.title, font: titleFont, textColor: titleColor)
let titleSize = self.titleNode.updateLayout(availableSize)
transition.setFrame(view: self.highlightNode.view, frame: CGRect(origin: CGPoint(x: -shadowInset, y: -shadowInset), size: CGSize(width: availableSize.width + shadowInset * 2.0, height: availableSize.height + shadowInset * 2.0)))
let titleImage = imageCache.value.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title)
self.titleView.image = titleImage
let titleSize = titleImage.size
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
transition.setFrame(view: self.highlightView, frame: contentFrame)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: availableSize)
self.buttonNode.isEnabled = component.isEnabled && component.media != nil
self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
if let mediaPreviewNode = self.mediaPreviewNode {
mediaPreviewNode.frame = CGRect(origin: CGPoint(), size: availableSize)
mediaPreviewNode.updateLayout(size: availableSize, synchronousLoads: false)
self.button.frame = CGRect(origin: CGPoint(), size: availableSize)
self.button.isEnabled = component.isEnabled && component.media != nil
if let mediaPreviewView = self.mediaPreviewView {
mediaPreviewView.frame = contentFrame
mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false)
}
return availableSize
@ -382,6 +424,7 @@ private final class MonthComponent: CombinedComponent {
let weekdaySize: CGFloat = 46.0
let weekdaySpacing: CGFloat = 6.0
let usableWeekdayWidth = floor((context.availableSize.width - sideInset * 2.0 - weekdaySpacing * 6.0) / 7.0)
let weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0)
let title = title.update(
@ -440,7 +483,7 @@ private final class MonthComponent: CombinedComponent {
environment: {
context.environment[ImageCache.self]
},
availableSize: CGSize(width: weekdaySize, height: weekdaySize),
availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize),
transition: .immediate
)
}
@ -843,7 +886,7 @@ public final class CalendarMessageScreen: ViewController {
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
//TODO:localize
self.navigationItem.setTitle("Jump to Date", animated: false)
self.navigationItem.setTitle("Calendar", animated: false)
}
required public init(coder aDecoder: NSCoder) {

View File

@ -2,6 +2,9 @@ import Foundation
import UIKit
public final class ComponentHostView<EnvironmentType>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>?
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<EnvironmentType>: UIView {
}
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, 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<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, containerSize: CGSize) -> CGSize {

View File

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

View File

@ -16,6 +16,7 @@ swift_library(
"//submodules/TinyThumbnail:TinyThumbnail",
"//submodules/Display:Display",
"//submodules/FastBlur:FastBlur",
"//submodules/MozjpegBinding:MozjpegBinding",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

@ -206,3 +206,183 @@ open class TransformImageNode: ASDisplayNode {
}
}
}
open class TransformImageView: UIView {
public var imageUpdated: ((UIImage?) -> Void)?
public var contentAnimations: TransformImageNodeContentAnimations = []
private var disposable = MetaDisposable()
private var currentTransform: ((TransformImageArguments) -> DrawingContext?)?
private var currentArguments: TransformImageArguments?
private var argumentsPromise = ValuePromise<TransformImageArguments>(ignoreRepeated: true)
private var overlayColor: UIColor?
private var overlayView: UIView?
override public init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.accessibilityIgnoresInvertColors = true
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
}
override open var frame: CGRect {
didSet {
if let overlayView = self.overlayView {
overlayView.frame = self.bounds
}
}
}
public func reset() {
self.disposable.set(nil)
self.currentArguments = nil
self.currentTransform = nil
self.layer.contents = nil
}
public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true) {
let argumentsPromise = self.argumentsPromise
let data = combineLatest(signal, argumentsPromise.get())
let resultData: Signal<((TransformImageArguments) -> DrawingContext?, TransformImageArguments), NoError>
if attemptSynchronously {
resultData = data
} else {
resultData = data
|> deliverOn(Queue.concurrentDefaultQueue())
}
let result = resultData
|> mapToThrottled { transform, arguments -> Signal<((TransformImageArguments) -> DrawingContext?, TransformImageArguments, UIImage?)?, NoError> in
return deferred {
if let context = transform(arguments) {
return .single((transform, arguments, context.generateImage()))
} else {
return .single(nil)
}
}
}
self.disposable.set((result |> deliverOnMainQueue).start(next: { [weak self] next in
let apply: () -> Void = {
if let strongSelf = self {
if strongSelf.layer.contents == nil {
if strongSelf.contentAnimations.contains(.firstUpdate) && !attemptSynchronously {
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
} else if strongSelf.contentAnimations.contains(.subsequentUpdates) {
let tempLayer = CALayer()
tempLayer.frame = strongSelf.bounds
tempLayer.contentsGravity = strongSelf.layer.contentsGravity
tempLayer.contents = strongSelf.layer.contents
strongSelf.layer.addSublayer(tempLayer)
tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in
tempLayer?.removeFromSuperlayer()
})
}
var imageUpdate: UIImage?
if let (transform, arguments, image) = next {
strongSelf.currentTransform = transform
strongSelf.currentArguments = arguments
strongSelf.layer.contents = image?.cgImage
imageUpdate = image
}
if let _ = strongSelf.overlayColor {
strongSelf.applyOverlayColor(animated: false)
}
if let imageUpdated = strongSelf.imageUpdated {
imageUpdated(imageUpdate)
}
}
}
if dispatchOnDisplayLink && !attemptSynchronously {
displayLinkDispatcher.dispatch {
apply()
}
} else {
apply()
}
}))
}
public func asyncLayout() -> (TransformImageArguments) -> (() -> Void) {
let currentTransform = self.currentTransform
let currentArguments = self.currentArguments
return { [weak self] arguments in
let updatedImage: UIImage?
if currentArguments != arguments {
updatedImage = currentTransform?(arguments)?.generateImage()
} else {
updatedImage = nil
}
return {
guard let strongSelf = self else {
return
}
if let image = updatedImage {
strongSelf.layer.contents = image.cgImage
strongSelf.currentArguments = arguments
if let _ = strongSelf.overlayColor {
strongSelf.applyOverlayColor(animated: false)
}
}
strongSelf.argumentsPromise.set(arguments)
}
}
}
public func setOverlayColor(_ color: UIColor?, animated: Bool) {
var updated = false
if let overlayColor = self.overlayColor, let color = color {
updated = !overlayColor.isEqual(color)
} else if (self.overlayColor != nil) != (color != nil) {
updated = true
}
if updated {
self.overlayColor = color
if let _ = self.overlayColor {
self.applyOverlayColor(animated: animated)
} else if let overlayView = self.overlayView {
self.overlayView = nil
if animated {
overlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak overlayView] _ in
overlayView?.removeFromSuperview()
})
} else {
overlayView.removeFromSuperview()
}
}
}
}
private func applyOverlayColor(animated: Bool) {
if let overlayColor = self.overlayColor {
if let contents = self.layer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
if let overlayView = self.overlayView {
(overlayView as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate)
overlayView.tintColor = overlayColor
} else {
let overlayView = UIImageView()
overlayView.image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate)
overlayView.tintColor = overlayColor
overlayView.frame = self.bounds
self.addSubview(overlayView)
self.overlayView = overlayView
}
}
}
}
}

View File

@ -184,7 +184,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
private let playbackStatusDisposable = MetaDisposable()
private let playbackStatus = Promise<MediaPlayerStatus>()
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

View File

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

View File

@ -3,3 +3,4 @@
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size);
UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData);

View File

@ -2,6 +2,7 @@
#import <mozjpeg/turbojpeg.h>
#import <mozjpeg/jpeglib.h>
#import <Accelerate/Accelerate.h>
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;
}

View File

@ -14,6 +14,7 @@ swift_library(
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimationUI:AnimationUI",
],
visibility = [
"//visibility:public",

View File

@ -13,17 +13,32 @@ private let nullAction = NullActionClass()
public protocol SparseItemGridLayer: CALayer {
func update(size: CGSize)
func needsShimmer() -> Bool
}
public protocol SparseItemGridView: UIView {
func update(size: CGSize)
func needsShimmer() -> Bool
}
public protocol SparseItemGridDisplayItem: AnyObject {
var layer: SparseItemGridLayer? { get }
var view: SparseItemGridView? { get }
}
public protocol SparseItemGridBinding: AnyObject {
func createLayer() -> SparseItemGridLayer
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer])
func createLayer() -> SparseItemGridLayer?
func createView() -> SparseItemGridView?
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], synchronous: Bool)
func unbindLayer(layer: SparseItemGridLayer)
func scrollerTextForTag(tag: Int32) -> String?
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
func onTap(item: SparseItemGrid.Item)
func onTagTap()
func didScroll()
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
func onBeginFastScrolling()
func getShimmerColors() -> SparseItemGrid.ShimmerColors
}
private func binarySearch(_ inputArr: [SparseItemGrid.Item], searchItem: Int) -> (index: Int?, lowerBound: Int?, upperBound: Int?) {
@ -78,24 +93,69 @@ private func binarySearch(_ inputArr: [SparseItemGrid.HoleAnchor], searchItem: I
}
}
public final class SparseItemGrid: ASDisplayNode {
public final class ShimmerLayer: CAGradientLayer {
override public init() {
super.init()
private final class Shimmer {
private var image: UIImage?
private var colors: SparseItemGrid.ShimmerColors = SparseItemGrid.ShimmerColors(background: 0, foreground: 0)
self.backgroundColor = UIColor(white: 0.9, alpha: 1.0).cgColor
func update(colors: SparseItemGrid.ShimmerColors, layer: CALayer, containerSize: CGSize, frame: CGRect) {
if self.colors != colors {
self.colors = colors
self.image = generateImage(CGSize(width: 1.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: colors.background).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = UIColor(argb: colors.foreground).withAlphaComponent(0.0).cgColor
let peakColor = UIColor(argb: colors.foreground).cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
if let image = self.image {
layer.contents = image.cgImage
override public func action(forKey event: String) -> CAAction? {
let shiftedContentsRect = CGRect(origin: CGPoint(x: frame.minX / containerSize.width, y: frame.minY / containerSize.height), size: CGSize(width: frame.width / containerSize.width, height: frame.height / containerSize.height))
let _ = shiftedContentsRect
layer.contentsRect = shiftedContentsRect
if layer.animation(forKey: "shimmer") == nil {
let animation = CABasicAnimation(keyPath: "contentsRect.origin.y")
animation.fromValue = 1.0 as NSNumber
animation.toValue = -1.0 as NSNumber
animation.isAdditive = true
animation.repeatCount = .infinity
animation.duration = 0.8
animation.beginTime = 1.0
layer.add(animation, forKey: "shimmer")
}
}
}
final class Layer: CALayer {
override func action(forKey event: String) -> CAAction? {
return nullAction
}
}
}
func update(size: CGSize) {
self.endPoint = CGPoint(x: 0.0, y: size.height)
public final class SparseItemGrid: ASDisplayNode {
public struct ShimmerColors: Equatable {
public var background: UInt32
public var foreground: UInt32
public init(background: UInt32, foreground: UInt32) {
self.background = background
self.foreground = foreground
}
}
@ -258,40 +318,84 @@ public final class SparseItemGrid: ASDisplayNode {
}
private final class Viewport: ASDisplayNode, UIScrollViewDelegate {
final class VisibleItemLayer {
let layer: SparseItemGridLayer
final class VisibleItem: SparseItemGridDisplayItem {
let layer: SparseItemGridLayer?
let view: SparseItemGridView?
init(layer: SparseItemGridLayer) {
init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
self.layer = layer
self.view = view
}
var displayLayer: CALayer {
if let layer = self.layer {
return layer
} else if let view = self.view {
return view.layer
} else {
preconditionFailure()
}
}
var frame: CGRect {
get {
return self.displayLayer.frame
} set(value) {
if let layer = self.layer {
layer.frame = value
} else if let view = self.view {
view.frame = value
} else {
preconditionFailure()
}
}
}
var needsShimmer: Bool {
if let layer = self.layer {
return layer.needsShimmer()
} else if let view = self.view {
return view.needsShimmer()
} else {
preconditionFailure()
}
}
}
final class Layout {
let containerLayout: ContainerLayout
let itemSize: CGFloat
let itemSize: CGSize
let itemSpacing: CGFloat
let lastItemSize: CGFloat
let itemsPerRow: Int
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
self.containerLayout = containerLayout
self.itemSpacing = 1.0
if let fixedItemHeight = containerLayout.fixedItemHeight {
self.itemsPerRow = 1
self.itemSize = CGSize(width: containerLayout.size.width, height: fixedItemHeight)
self.lastItemSize = containerLayout.size.width
self.itemSpacing = 0.0
} else {
self.itemSpacing = 1.0
let width = containerLayout.size.width
let baseItemWidth = floor(min(150.0, width / 3.0))
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
let itemsPerRow = floor(width / unclippedItemWidth)
self.itemsPerRow = Int(itemsPerRow)
self.itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
let width = containerLayout.size.width
let baseItemWidth = floor(min(150.0, width / 3.0))
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
let itemsPerRow = floor(width / unclippedItemWidth)
self.itemsPerRow = Int(itemsPerRow)
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
self.itemSize = CGSize(width: itemSize, height: itemSize)
self.lastItemSize = width - (self.itemSize + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
}
}
func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow
let column = index % self.itemsPerRow
return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize + self.itemSpacing), y: CGFloat(row) * (self.itemSize + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize, height: itemSize))
return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: self.containerLayout.insets.top + CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
}
func contentHeight(count: Int) -> CGFloat {
@ -299,9 +403,10 @@ public final class SparseItemGrid: ASDisplayNode {
}
func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) {
var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.containerLayout.insets.top)
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing)))
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
@ -313,16 +418,22 @@ public final class SparseItemGrid: ASDisplayNode {
let zoomLevel: ZoomLevel
private let scrollView: UIScrollView
private let shimmer: Shimmer
var layout: Layout?
var items: Items?
var visibleItems: [AnyHashable: VisibleItemLayer] = [:]
var visiblePlaceholders: [ShimmerLayer] = []
var visibleItems: [AnyHashable: VisibleItem] = [:]
var visiblePlaceholders: [Shimmer.Layer] = []
private var scrollingArea: SparseItemGridScrollingArea?
private var currentScrollingTag: Int32?
private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void
private var ignoreScrolling: Bool = false
private var isFastScrolling: Bool = false
private var previousScrollOffset: CGFloat = 0.0
var coveringInsetOffset: CGFloat = 0.0
init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void) {
self.zoomLevel = zoomLevel
@ -338,6 +449,8 @@ public final class SparseItemGrid: ASDisplayNode {
self.scrollView.delaysContentTouches = false
self.scrollView.clipsToBounds = false
self.shimmer = Shimmer()
super.init()
self.anchorPoint = CGPoint()
@ -351,13 +464,105 @@ public final class SparseItemGrid: ASDisplayNode {
self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel)
self.items = items
self.updateVisibleItems(resetScrolling: true, restoreScrollPosition: restoreScrollPosition)
self.updateVisibleItems(resetScrolling: true, synchronous: false, restoreScrollPosition: restoreScrollPosition)
}
}
@objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.items?.itemBinding.didScroll()
}
@objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(resetScrolling: false, restoreScrollPosition: nil)
self.updateVisibleItems(resetScrolling: false, synchronous: true, restoreScrollPosition: nil)
if let layout = self.layout, let items = self.items {
let offset = scrollView.contentOffset.y
let delta = offset - self.previousScrollOffset
self.previousScrollOffset = offset
if self.isFastScrolling {
if offset <= layout.containerLayout.insets.top {
var coveringInsetOffset = self.coveringInsetOffset + delta
if coveringInsetOffset < 0.0 {
coveringInsetOffset = 0.0
}
if coveringInsetOffset > layout.containerLayout.insets.top {
coveringInsetOffset = layout.containerLayout.insets.top
}
if offset <= 0.0 {
coveringInsetOffset = 0.0
}
if coveringInsetOffset < self.coveringInsetOffset {
self.coveringInsetOffset = coveringInsetOffset
items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate)
}
}
} else {
var coveringInsetOffset = self.coveringInsetOffset + delta
if coveringInsetOffset < 0.0 {
coveringInsetOffset = 0.0
}
if coveringInsetOffset > layout.containerLayout.insets.top {
coveringInsetOffset = layout.containerLayout.insets.top
}
if offset <= 0.0 {
coveringInsetOffset = 0.0
}
if coveringInsetOffset != self.coveringInsetOffset {
self.coveringInsetOffset = coveringInsetOffset
items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate)
}
}
}
}
}
@objc func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.snapCoveringInsetOffset()
}
}
@objc func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !self.ignoreScrolling {
if !decelerate {
self.snapCoveringInsetOffset()
}
}
}
@objc func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.snapCoveringInsetOffset()
}
}
private func snapCoveringInsetOffset() {
if let layout = self.layout, let items = self.items {
let offset = self.scrollView.contentOffset.y
if offset < layout.containerLayout.insets.top {
if offset <= layout.containerLayout.insets.top / 2.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
} else {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: layout.containerLayout.insets.top), animated: true)
}
} else {
var coveringInsetOffset = self.coveringInsetOffset
if coveringInsetOffset > layout.containerLayout.insets.top / 2.0 {
coveringInsetOffset = layout.containerLayout.insets.top
} else {
coveringInsetOffset = 0.0
}
if offset <= 0.0 {
coveringInsetOffset = 0.0
}
if coveringInsetOffset != self.coveringInsetOffset {
self.coveringInsetOffset = coveringInsetOffset
items.itemBinding.coveringInsetOffsetUpdated(transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
}
@ -369,7 +574,7 @@ public final class SparseItemGrid: ASDisplayNode {
let localPoint = self.scrollView.convert(point, from: self.view)
for (id, visibleItem) in self.visibleItems {
if visibleItem.layer.frame.contains(localPoint) {
if visibleItem.frame.contains(localPoint) {
for item in items.items {
if item.id == id {
return item
@ -391,7 +596,7 @@ public final class SparseItemGrid: ASDisplayNode {
var closestItem: (CGFloat, AnyHashable)?
for (id, visibleItem) in self.visibleItems {
let itemCenter = visibleItem.layer.frame.center
let itemCenter = visibleItem.frame.center
let distanceX = itemCenter.x - localPoint.x
let distanceY = itemCenter.y - localPoint.y
let distance2 = distanceX * distanceX + distanceY * distanceY
@ -446,12 +651,22 @@ public final class SparseItemGrid: ASDisplayNode {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
}
private func updateVisibleItems(resetScrolling: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) {
func scrollToTop() -> Bool {
if self.scrollView.contentOffset.y > 0.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
return true
} else {
return false
}
}
private func updateVisibleItems(resetScrolling: Bool, synchronous: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) {
guard let layout = self.layout, let items = self.items else {
return
}
let contentHeight = layout.contentHeight(count: items.count)
let shimmerColors = items.itemBinding.getShimmerColors()
if resetScrolling {
if !self.scrollView.bounds.isEmpty {
@ -494,49 +709,65 @@ public final class SparseItemGrid: ASDisplayNode {
var usedPlaceholderCount = 0
if !items.items.isEmpty {
var bindItems: [Item] = []
var bindLayers: [SparseItemGridLayer] = []
var updateLayers: [SparseItemGridLayer] = []
var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridDisplayItem] = []
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
for index in visibleRange.minIndex ... visibleRange.maxIndex {
if let item = items.item(at: index) {
let itemLayer: VisibleItemLayer
let itemLayer: VisibleItem
if let current = self.visibleItems[item.id] {
itemLayer = current
updateLayers.append(itemLayer.layer)
updateLayers.append(itemLayer)
} else {
itemLayer = VisibleItemLayer(layer: items.itemBinding.createLayer())
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
self.visibleItems[item.id] = itemLayer
bindItems.append(item)
bindLayers.append(itemLayer.layer)
bindLayers.append(itemLayer)
self.scrollView.layer.addSublayer(itemLayer.layer)
if let layer = itemLayer.layer {
self.scrollView.layer.addSublayer(layer)
} else if let view = itemLayer.view {
self.scrollView.addSubview(view)
}
}
validIds.insert(item.id)
itemLayer.layer.frame = layout.frame(at: index)
} else {
let placeholderLayer: ShimmerLayer
itemLayer.frame = layout.frame(at: index)
} else if layout.containerLayout.fixedItemHeight == nil {
let placeholderLayer: Shimmer.Layer
if self.visiblePlaceholders.count > usedPlaceholderCount {
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
} else {
placeholderLayer = ShimmerLayer()
placeholderLayer = Shimmer.Layer()
self.scrollView.layer.addSublayer(placeholderLayer)
self.visiblePlaceholders.append(placeholderLayer)
}
placeholderLayer.frame = layout.frame(at: index)
let itemFrame = layout.frame(at: index)
placeholderLayer.frame = itemFrame
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
usedPlaceholderCount += 1
}
}
if !bindItems.isEmpty {
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers)
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, synchronous: synchronous)
}
for layer in updateLayers {
layer.update(size: layer.bounds.size)
for item in updateLayers {
let item = item as! VisibleItem
if let layer = item.layer {
layer.update(size: layer.frame.size)
} else if let view = item.view {
view.update(size: layer.frame.size)
}
if item.needsShimmer {
let itemFrame = layer.frame
self.shimmer.update(colors: shimmerColors, layer: item.displayLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
}
}
}
@ -547,9 +778,13 @@ public final class SparseItemGrid: ASDisplayNode {
}
}
for id in removeIds {
if let itemLayer = self.visibleItems.removeValue(forKey: id) {
items.itemBinding.unbindLayer(layer: itemLayer.layer)
itemLayer.layer.removeFromSuperlayer()
if let item = self.visibleItems.removeValue(forKey: id) {
if let layer = item.layer {
items.itemBinding.unbindLayer(layer: layer)
layer.removeFromSuperlayer()
} else if let view = item.view {
view.removeFromSuperview()
}
}
}
@ -600,8 +835,17 @@ public final class SparseItemGrid: ASDisplayNode {
guard let strongSelf = self else {
return nil
}
strongSelf.items?.itemBinding.onBeginFastScrolling()
return strongSelf.scrollView
}
scrollingArea.setContentOffset = { [weak self] offset in
guard let strongSelf = self else {
return
}
strongSelf.isFastScrolling = true
strongSelf.scrollView.setContentOffset(offset, animated: false)
strongSelf.isFastScrolling = false
}
self.updateScrollingArea()
}
}
@ -624,13 +868,20 @@ public final class SparseItemGrid: ASDisplayNode {
}
if let scrollingArea = self.scrollingArea {
let dateString = tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) }
if self.currentScrollingTag != tag {
self.currentScrollingTag = tag
if scrollingArea.isDragging {
scrollingArea.feedbackTap()
}
}
scrollingArea.update(
containerSize: layout.containerLayout.size,
containerInsets: layout.containerLayout.insets,
contentHeight: contentHeight,
contentOffset: self.scrollView.bounds.minY,
isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating,
dateString: tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } ?? "",
dateString: dateString ?? "",
transition: .immediate
)
}
@ -740,8 +991,10 @@ public final class SparseItemGrid: ASDisplayNode {
var insets: UIEdgeInsets
var scrollIndicatorInsets: UIEdgeInsets
var lockScrollingAtTop: Bool
var fixedItemHeight: CGFloat?
}
private var tapRecognizer: UITapGestureRecognizer?
private var pinchRecognizer: UIPinchGestureRecognizer?
private var containerLayout: ContainerLayout?
@ -754,6 +1007,13 @@ public final class SparseItemGrid: ASDisplayNode {
private var isLoadingHole: Bool = false
private let loadingHoleDisposable = MetaDisposable()
public var coveringInsetOffset: CGFloat {
if let currentViewport = self.currentViewport {
return currentViewport.coveringInsetOffset
}
return 0.0
}
override public init() {
self.scrollingArea = SparseItemGridScrollingArea()
@ -762,6 +1022,7 @@ public final class SparseItemGrid: ASDisplayNode {
self.clipsToBounds = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.view.addGestureRecognizer(tapRecognizer)
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
@ -936,12 +1197,15 @@ public final class SparseItemGrid: ASDisplayNode {
}
}
public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, items: Items) {
let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop)
public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items) {
let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight)
self.containerLayout = containerLayout
self.items = items
self.scrollingArea.isHidden = lockScrollingAtTop
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
if self.currentViewport == nil {
let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
guard let strongSelf = self else {
@ -1063,12 +1327,12 @@ public final class SparseItemGrid: ASDisplayNode {
}
}
public func forEachVisibleItem(_ f: (SparseItemGridLayer) -> Void) {
public func forEachVisibleItem(_ f: (SparseItemGridDisplayItem) -> Void) {
guard let currentViewport = self.currentViewport else {
return
}
for (_, itemLayer) in currentViewport.visibleItems {
f(itemLayer.layer)
f(itemLayer)
}
}
@ -1086,7 +1350,18 @@ public final class SparseItemGrid: ASDisplayNode {
currentViewport.scrollToItem(at: index)
}
public func scrollToTop() -> Bool {
guard let currentViewport = self.currentViewport else {
return false
}
return currentViewport.scrollToTop()
}
public func addToTransitionSurface(view: UIView) {
self.view.insertSubview(view, belowSubview: self.scrollingArea.view)
}
public func updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip) {
self.scrollingArea.displayTooltip = tooltip
}
}

View File

@ -4,6 +4,263 @@ import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AnimationUI
public final class MultilineText: Component {
public let text: String
public let font: UIFont
public let color: UIColor
public init(
text: String,
font: UIFont,
color: UIColor
) {
self.text = text
self.font = font
self.color = color
}
public static func ==(lhs: MultilineText, rhs: MultilineText) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIView {
private let text: ImmediateTextNode
init() {
self.text = ImmediateTextNode()
self.text.maximumNumberOfLines = 0
super.init(frame: CGRect())
self.addSubnode(self.text)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: MultilineText, availableSize: CGSize, environment: Environment<Empty>, 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<Empty>, 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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
public final class TooltipComponent: Component {
public let icon: AnyComponent<Empty>?
public let content: AnyComponent<Empty>
public let arrowLocation: CGRect
public init(
icon: AnyComponent<Empty>?,
content: AnyComponent<Empty>,
arrowLocation: CGRect
) {
self.icon = icon
self.content = content
self.arrowLocation = arrowLocation
}
public static func ==(lhs: TooltipComponent, rhs: TooltipComponent) -> Bool {
if lhs.icon != rhs.icon {
return false
}
if lhs.content != rhs.content {
return false
}
if lhs.arrowLocation != rhs.arrowLocation {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: UIView
private let backgroundViewMask: UIImageView
private var icon: ComponentHostView<Empty>?
private let content: ComponentHostView<Empty>
init() {
self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.backgroundViewMask = UIImageView()
self.backgroundViewMask.image = generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor)
let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ")
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 34)
self.content = ComponentHostView<Empty>()
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.backgroundView.mask = self.backgroundViewMask
self.addSubview(self.content)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: TooltipComponent, availableSize: CGSize, environment: Environment<Empty>, 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<Empty>
if let current = self.icon {
iconView = current
} else {
iconView = ComponentHostView<Empty>()
self.icon = iconView
self.addSubview(iconView)
}
iconSize = iconView.update(
transition: transition,
component: icon,
environment: {},
containerSize: availableSize
)
} else if let icon = self.icon {
self.icon = nil
icon.removeFromSuperview()
}
var contentLeftInset: CGFloat = 0.0
if let iconSize = iconSize {
contentLeftInset += iconSize.width + spacing
}
let contentSize = self.content.update(
transition: transition,
component: component.content,
environment: {},
containerSize: CGSize(width: min(200.0, availableSize.width - contentLeftInset), height: availableSize.height)
)
var innerContentHeight = contentSize.height
if let iconSize = iconSize, iconSize.height > innerContentHeight {
innerContentHeight = iconSize.height
}
let combinedContentSize = CGSize(width: insets.left + insets.right + contentLeftInset + contentSize.width, height: insets.top + insets.bottom + innerContentHeight)
var contentRect = CGRect(origin: CGPoint(x: component.arrowLocation.minX - combinedContentSize.width, y: component.arrowLocation.maxY), size: combinedContentSize)
if contentRect.minX < 0.0 {
contentRect.origin.x = component.arrowLocation.maxX
}
if contentRect.minY < 0.0 {
contentRect.origin.y = component.arrowLocation.minY - contentRect.height
}
let maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 4.0))
self.backgroundView.frame = maskedBackgroundFrame
self.backgroundViewMask.frame = CGRect(origin: CGPoint(), size: maskedBackgroundFrame.size)
if let iconSize = iconSize, let icon = self.icon {
transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize))
}
transition.setFrame(view: self.content, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left + contentLeftInset, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - contentSize.height) / 2.0)), size: contentSize))
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
private final class RoundedRectangle: Component {
let color: UIColor
@ -134,7 +391,7 @@ private final class ShadowRoundedRectangle: Component {
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(component.color.cgColor)
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 5.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor)
context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 4.0, color: UIColor(white: 0.0, alpha: 0.2).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0)))
})?.stretchableImage(withLeftCapWidth: Int(diameter + shadowInset * 2.0) / 2, topCapHeight: Int(diameter + shadowInset * 2.0) / 2)
@ -324,6 +581,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
private let dateIndicator: ComponentHostView<Empty>
private let lineIndicator: ComponentHostView<Empty>
private var displayedTooltip: Bool = false
private var lineTooltip: ComponentHostView<Empty>?
private var containerSize: CGSize?
private var indicatorPosition: CGFloat?
private var scrollIndicatorHeight: CGFloat?
@ -336,6 +598,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
private var activityTimer: SwiftSignalKit.Timer?
public var beginScrolling: (() -> UIScrollView?)?
public var setContentOffset: ((CGPoint) -> Void)?
public var openCurrentDate: (() -> Void)?
private var offsetBarTimer: SwiftSignalKit.Timer?
@ -350,6 +613,20 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
}
private var projectionData: ProjectionData?
public struct DisplayTooltip {
public var animation: String?
public var text: String
public var completed: () -> Void
public init(animation: String?, text: String, completed: @escaping () -> Void) {
self.animation = animation
self.text = text
self.completed = completed
}
}
public var displayTooltip: DisplayTooltip?
override public init() {
self.dateIndicator = ComponentHostView<Empty>()
self.lineIndicator = ComponentHostView<Empty>()
@ -399,10 +676,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
if let scrollView = strongSelf.beginScrolling?() {
strongSelf.draggingScrollView = scrollView
strongSelf.scrollingInitialOffset = scrollView.contentOffset.y
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
strongSelf.setContentOffset?(scrollView.contentOffset)
}
strongSelf.updateActivityTimer()
strongSelf.updateActivityTimer(isScrolling: false)
},
ended: { [weak self] in
guard let strongSelf = self else {
@ -424,7 +701,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
strongSelf.updateLineIndicator(transition: transition)
strongSelf.updateActivityTimer()
strongSelf.updateActivityTimer(isScrolling: false)
},
moved: { [weak self] relativeOffset in
guard let strongSelf = self else {
@ -454,7 +731,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
offset = scrollView.contentSize.height - scrollView.bounds.height
}
scrollView.setContentOffset(CGPoint(x: 0.0, y: offset), animated: false)
strongSelf.setContentOffset?(CGPoint(x: 0.0, y: offset))
let _ = scrollView
let _ = projectionData
}
@ -473,6 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
self.updateLineIndicator(transition: transition)
}
func feedbackTap() {
self.hapticFeedback.tap()
}
public func update(
containerSize: CGSize,
containerInsets: UIEdgeInsets,
@ -482,8 +763,15 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
dateString: String,
transition: ContainedViewLayoutTransition
) {
self.containerSize = containerSize
if self.dateIndicator.alpha.isZero {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint())
}
if isScrolling {
self.updateActivityTimer()
self.updateActivityTimer(isScrolling: true)
}
let indicatorSize = self.dateIndicator.update(
@ -508,18 +796,18 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
}
let indicatorVerticalInset: CGFloat = 3.0
let topIndicatorInset: CGFloat = indicatorVerticalInset
let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom
let scrollIndicatorHeight = max(35.0, ceil(scrollIndicatorHeightFraction * containerSize.height))
let scrollIndicatorHeight = max(44.0, ceil(scrollIndicatorHeightFraction * containerSize.height))
let indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height)))
let indicatorTopPosition = topIndicatorInset
let indicatorBottomPosition = containerSize.height - bottomIndicatorInset - scrollIndicatorHeight
let dateIndicatorTopPosition = topIndicatorInset
let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - indicatorSize.height
let dateIndicatorTopPosition = topIndicatorInset + 4.0
let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - 4.0 - indicatorSize.height
self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction
self.scrollIndicatorHeight = scrollIndicatorHeight
@ -535,11 +823,18 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
transition.updateFrame(view: self.dateIndicator, frame: CGRect(origin: CGPoint(x: containerSize.width - 12.0 - indicatorSize.width, y: dateIndicatorPosition), size: indicatorSize))
if isScrolling {
self.dateIndicator.alpha = 1.0
self.lineIndicator.alpha = 1.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0)
transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0)
}
self.updateLineTooltip(containerSize: containerSize)
self.updateLineIndicator(transition: transition)
if isScrolling {
self.displayTooltipOnFirstScroll()
}
}
private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
@ -547,7 +842,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
return
}
let lineIndicatorSize = CGSize(width: self.isDragging ? 6.0 : 3.0, height: scrollIndicatorHeight)
let lineIndicatorSize = CGSize(width: (self.isDragging || self.lineTooltip != nil) ? 6.0 : 3.0, height: scrollIndicatorHeight)
let mappedTransition: Transition
switch transition {
case .immediate:
@ -567,7 +862,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
transition.updateFrame(view: self.lineIndicator, frame: CGRect(origin: CGPoint(x: self.bounds.size.width - 3.0 - lineIndicatorSize.width, y: indicatorPosition), size: lineIndicatorSize))
}
private func updateActivityTimer() {
private func updateActivityTimer(isScrolling: Bool) {
self.activityTimer?.invalidate()
if self.isDragging {
@ -582,11 +877,71 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0)
if let lineTooltip = strongSelf.lineTooltip {
strongSelf.lineTooltip = nil
lineTooltip.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak lineTooltip] _ in
lineTooltip?.removeFromSuperview()
})
}
}, queue: .mainQueue())
self.activityTimer?.start()
}
}
private func displayTooltipOnFirstScroll() {
guard let displayTooltip = self.displayTooltip else {
return
}
if self.displayedTooltip {
return
}
self.displayedTooltip = true
let lineTooltip = ComponentHostView<Empty>()
self.lineTooltip = lineTooltip
self.view.addSubview(lineTooltip)
if let containerSize = self.containerSize {
self.updateLineTooltip(containerSize: containerSize)
}
lineTooltip.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let transition: ContainedViewLayoutTransition = .immediate
transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint(x: -3.0, y: 0.0))
displayTooltip.completed()
}
private func updateLineTooltip(containerSize: CGSize) {
guard let displayTooltip = self.displayTooltip else {
return
}
guard let lineTooltip = self.lineTooltip else {
return
}
let lineTooltipSize = lineTooltip.update(
transition: .immediate,
component: AnyComponent(TooltipComponent(
icon: displayTooltip.animation.flatMap { animation in
AnyComponent(LottieAnimationComponent(
name: animation
))
},
content: AnyComponent(MultilineText(
text: displayTooltip.text,
font: Font.regular(13.0),
color: .white
)),
arrowLocation: self.lineIndicator.frame.insetBy(dx: -3.0, dy: -8.0)
)),
environment: {},
containerSize: containerSize
)
lineTooltip.frame = CGRect(origin: CGPoint(), size: lineTooltipSize)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.dateIndicator.alpha <= 0.01 {
return nil

View File

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

View File

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

View File

@ -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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<[Int64]?, NoError> {
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks())

View File

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

View File

@ -7,7 +7,6 @@ import Intents
import Postbox
import PushKit
import AsyncDisplayKit
import CloudKit
import TelegramUIPreferences
import TelegramPresentationData
import TelegramCallsUI

View File

@ -922,9 +922,34 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView
let displayRange = strongSelf.displayedItemRange
if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange {
let firstEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.firstIndex]
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
var anchorIndex: MessageIndex?
loop: for index in visibleRange.firstIndex ..< filteredEntries.count {
switch filteredEntries[filteredEntries.count - 1 - index] {
case let .MessageEntry(message, _, _, _, _, _):
if message.adAttribute == nil {
anchorIndex = message.index
break loop
}
case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _) in messages {
if message.adAttribute == nil {
anchorIndex = message.index
break loop
}
}
default:
break
}
}
if anchorIndex == nil, let historyView = historyView {
for entry in historyView.originalView.entries {
anchorIndex = entry.message.index
break
}
}
if let anchorIndex = anchorIndex {
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(anchorIndex), anchorIndex: .message(anchorIndex), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
}
} else {
if let subject = subject, case let .message(messageId, highlight, _) = subject {
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)

View File

@ -81,6 +81,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
var status: Signal<PeerInfoStatusData?, NoError> {
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

View File

@ -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<Bool>()
private var didSetReady: Bool = false
@ -54,6 +54,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get()
}
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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:

View File

@ -124,6 +124,11 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
var status: Signal<PeerInfoStatusData?, NoError> {
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

View File

@ -1044,7 +1044,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable {
}
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private(set) var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = []
@ -1666,6 +1666,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleNodeContainer: ASDisplayNode
let subtitleNodeRawContainer: ASDisplayNode
let subtitleNode: MultiScaleTextNode
let panelSubtitleNode: MultiScaleTextNode
let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode
@ -1720,6 +1721,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleNodeRawContainer = ASDisplayNode()
self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.subtitleNode.displaysAsynchronously = false
self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.panelSubtitleNode.displaysAsynchronously = false
self.usernameNodeContainer = ASDisplayNode()
self.usernameNodeRawContainer = ASDisplayNode()
@ -1770,6 +1774,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.titleNodeContainer.addSubnode(self.titleNode)
self.regularContentNode.addSubnode(self.titleNodeContainer)
self.subtitleNodeContainer.addSubnode(self.subtitleNode)
self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode)
self.regularContentNode.addSubnode(self.subtitleNodeContainer)
self.regularContentNode.addSubnode(self.subtitleNodeRawContainer)
self.usernameNodeContainer.addSubnode(self.usernameNode)
@ -1899,7 +1904,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
var initializedCredibilityIcon = false
func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat {
func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: PeerInfoStatusData?, isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat {
self.state = state
self.peer = peer
self.avatarListNode.listContainerNode.peer = peer
@ -2019,6 +2024,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var isVerified = false
let titleString: NSAttributedString
let subtitleString: NSAttributedString
var panelSubtitleString: NSAttributedString?
let usernameString: NSAttributedString
if let peer = peer, peer.isVerified {
isVerified = true
@ -2063,6 +2069,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let panelStatusData = panelStatusData {
let subtitleColor: UIColor
if panelStatusData.isActivity {
subtitleColor = presentationData.theme.list.itemAccentColor
} else {
subtitleColor = presentationData.theme.list.itemSecondaryTextColor
}
panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(15.0), textColor: subtitleColor)
}
} else {
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
@ -2089,6 +2105,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
TitleNodeStateExpanded: MultiScaleTextState(attributedText: subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height))
], mainState: TitleNodeStateRegular)
self.subtitleNode.accessibilityLabel = subtitleString.string
let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height))
], mainState: TitleNodeStateRegular)
self.panelSubtitleNode.accessibilityLabel = (panelSubtitleString ?? subtitleString).string
let usernameNodeLayout = self.usernameNode.updateLayout(states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
@ -2102,6 +2124,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size
let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size
let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size
let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size
let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size
if let image = self.titleCredibilityIconNode.image {
@ -2150,17 +2173,53 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let avatarMinScale: CGFloat = 0.7
let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset
let paneAreaExpansionDistance: CGFloat = 32.0
let effectiveAreaExpansionFraction: CGFloat
if state.isEditing {
effectiveAreaExpansionFraction = 0.0
} else if isSettings {
var paneAreaExpansionDelta = (self.frame.maxY - navigationHeight) - contentOffset
paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance))
effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance
} else {
var paneAreaExpansionDelta = (paneContainerY - navigationHeight) - contentOffset
paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance))
effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance
}
self.titleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], transition: transition)
let subtitleAlpha: CGFloat = self.isSettings ? 1.0 - titleCollapseFraction : 1.0
let subtitleAlpha: CGFloat
var subtitleOffset: CGFloat = 0.0
let panelSubtitleAlpha: CGFloat
var panelSubtitleOffset: CGFloat = 0.0
if self.isSettings {
subtitleAlpha = 1.0 - titleCollapseFraction
panelSubtitleAlpha = 0.0
} else {
if (panelSubtitleString ?? subtitleString).string != subtitleString.string {
subtitleAlpha = 1.0 - effectiveAreaExpansionFraction
panelSubtitleAlpha = effectiveAreaExpansionFraction
subtitleOffset = -effectiveAreaExpansionFraction * 5.0
panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0
} else {
subtitleAlpha = 1.0
panelSubtitleAlpha = 0.0
}
}
self.subtitleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], alpha: subtitleAlpha, transition: transition)
self.panelSubtitleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], alpha: panelSubtitleAlpha, transition: transition)
self.usernameNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
@ -2315,7 +2374,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size)
self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale)
@ -2353,7 +2413,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
usernameCenter.x = rawTitleFrame.center.x + (usernameCenter.x - rawTitleFrame.center.x) * subtitleScale
transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
}
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale)

View File

@ -15,8 +15,10 @@ protocol PeerInfoPaneNode: ASDisplayNode {
var parentController: ViewController? { get set }
var status: Signal<PeerInfoStatusData?, NoError> { get }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }
var tabBarOffset: CGFloat { get }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat)
func cancelPreviewGestures()
@ -32,21 +34,21 @@ final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey
let node: PeerInfoPaneNode
var isAnimatingOut: Bool = false
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
self.key = key
self.node = node
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
if let (currentSize, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
return
}
}
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
self.appliedParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
}
}
@ -401,13 +403,19 @@ private final class PeerInfoPendingPane {
paneDidScroll()
}
case .files:
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .files)
paneNode = visualPaneNode
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
case .links:
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage)
case .voice:
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .voiceAndVideoMessages)
paneNode = visualPaneNode
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
case .music:
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music)
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .music)
paneNode = visualPaneNode
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music)
case .gifs:
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
paneNode = visualPaneNode
@ -446,6 +454,7 @@ private final class PeerInfoPendingPane {
final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private let context: AccountContext
private let peerId: PeerId
private let isMediaOnly: Bool
weak var parentController: ViewController?
@ -469,6 +478,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
return nil
}
}
private let currentPaneStatusPromise = Promise<PeerInfoStatusData?>(nil)
var currentPaneStatus: Signal<PeerInfoStatusData?, NoError> {
return self.currentPaneStatusPromise.get()
}
private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:]
private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:]
@ -490,10 +504,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
private var currentAvailablePanes: [PeerInfoPaneKey]?
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId) {
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, isMediaOnly: Bool) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.peerId = peerId
self.isMediaOnly = isMediaOnly
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
@ -531,6 +546,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring))
strongSelf.currentPaneUpdated?(true)
strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil))
}
} else if strongSelf.pendingSwitchToPaneKey != key {
strongSelf.pendingSwitchToPaneKey = key
@ -634,6 +651,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.transitionFraction = 0.0
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring))
self.currentPaneUpdated?(false)
self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
}
default:
break
@ -675,6 +694,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.currentAvailablePanes = availablePanes
let previousCurrentPaneKey = self.currentPaneKey
var updateCurrentPaneStatus = false
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
var nextCandidatePaneKey: PeerInfoPaneKey?
@ -714,16 +734,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.coveringBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne
let tabsHeight: CGFloat = 48.0
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel)))
self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition)
transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight))
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
var visiblePaneIndices: [Int] = []
var requiredPendingKeys: [PeerInfoPaneKey] = []
@ -794,14 +810,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
)
self.pendingPanes[key] = pane
pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
let paneNode = pane.pane.node
pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in
guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else {
return
}
if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition)
}
}
leftScope = true
}
}
for (key, pane) in self.pendingPanes {
pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
if pane.isReady {
self.pendingPanes.removeValue(forKey: key)
@ -818,6 +843,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.pendingSwitchToPaneKey = nil
previousPaneKey = self.currentPaneKey
self.currentPaneKey = pendingSwitchToPaneKey
updateCurrentPaneStatus = true
updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey)
if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex {
if updatedCurrentIndex < previousIndex {
@ -834,7 +860,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
var paneWasAdded = false
if pane.node.supernode == nil {
self.addSubnode(pane.node)
self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode)
paneWasAdded = true
}
let indexOffset = CGFloat(index - updatedCurrentIndex)
@ -878,13 +904,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
paneCompletion()
})
}
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
}
}
//print("currentPanes: \(self.currentPanes.map { $0.0 })")
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight)))
var tabsOffset: CGFloat = 0.0
if let currentPane = self.currentPane {
tabsOffset = currentPane.node.tabBarOffset
}
tabsOffset = max(0.0, min(tabsHeight, tabsOffset))
if isScrollingLockedAtTop || self.isMediaOnly {
tabsOffset = 0.0
}
var tabsAlpha = 1.0 - tabsOffset / tabsHeight
tabsAlpha *= tabsAlpha
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight)))
transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel)))
self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition)
transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String
switch key {
@ -911,7 +953,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
for (_, pane) in self.pendingPanes {
let paneTransition: ContainedViewLayoutTransition = .immediate
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
}
if !self.didSetIsReady && data != nil {
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
@ -925,5 +967,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey {
self.currentPaneUpdated?(true)
}
if updateCurrentPaneStatus {
self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
}
}
}

View File

@ -61,6 +61,7 @@ import TelegramCallsUI
import PeerInfoAvatarListNode
import PasswordSetupUI
import CalendarMessageScreen
import TooltipUI
protocol PeerInfoScreenItem: AnyObject {
var id: AnyHashable { get }
@ -1617,7 +1618,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.scrollNode.canCancelAllTouchesInViews = true
self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isSettings: isSettings)
self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId)
self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, isMediaOnly: self.isMediaOnly)
super.init()
@ -2267,12 +2268,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
if let pane = strongSelf.paneContainerNode.currentPane?.node {
strongSelf.customStatusPromise.set(pane.status)
} else {
strongSelf.customStatusPromise.set(.single(nil))
}
if let (layout, navigationHeight) = strongSelf.validLayout {
if strongSelf.headerNode.isAvatarExpanded {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
@ -2291,6 +2286,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
}
self.customStatusPromise.set(self.paneContainerNode.currentPaneStatus)
self.paneContainerNode.requestExpandTabs = { [weak self] in
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
@ -6010,6 +6007,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
private weak var mediaGalleryContextMenu: ContextController?
func displaySharedMediaFastScrollingTooltip() {
guard let buttonNode = self.headerNode.navigationButtonContainer.buttonNodes[.more] else {
return
}
guard let controller = self.controller else {
return
}
let buttonFrame = buttonNode.view.convert(buttonNode.bounds, to: self.view)
//TODO:localize
controller.present(TooltipScreen(account: self.context.account, text: "Tap on this icon for calendar view", style: .default, icon: .none, location: .point(buttonFrame.insetBy(dx: 0.0, dy: 5.0), .top), shouldDismissOnTouch: { point in
return .dismiss(consume: false)
}), in: .current)
}
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) {
guard let controller = self.controller else {
return
@ -6027,12 +6038,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
if !"".isEmpty {
canZoom = false
}
/*if isZoomIn {
canZoom = pane?.availableZoomLevels().increment != nil
} else {
canZoom = pane?.availableZoomLevels().decrement != nil
}*/
return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "ZoomOut", textColor: canZoom ? .primary : .disabled, icon: { theme in
return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "Zoom Out", textColor: canZoom ? .primary : .disabled, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
}, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
@ -6094,8 +6100,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
updatedContentType = .photo
case .video:
updatedContentType = .photoOrVideo
case .gifs:
updatedContentType = .gifs
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
@ -6118,8 +6124,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
updatedContentType = .photoOrVideo
case .video:
updatedContentType = .video
case .gifs:
updatedContentType = .gifs
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
@ -6187,7 +6193,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
var contentHeight: CGFloat = 0.0
let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.customStatusData ?? self.data?.status, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive)
let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive)
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))
if additive {
transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame)
@ -6436,7 +6442,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
if let (layout, navigationHeight) = self.validLayout {
if !additive {
let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.customStatusData ?? self.data?.status, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive)
let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, transition: transition, additive: additive)
}
let paneAreaExpansionDistance: CGFloat = 32.0
@ -7308,7 +7314,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction)
var topHeight = topNavigationBar.backgroundNode.bounds.height
if let (layout, _) = self.screenNode.validLayout {
topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, transition: transition, additive: false)
topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: nil, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, transition: transition, additive: false)
}
let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.bounds.height

View File

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