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) 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]) 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 ComponentFlow
import PhotoResources import PhotoResources
private final class MediaPreviewNode: ASDisplayNode { private final class MediaPreviewView: UIView {
private let context: AccountContext private let context: AccountContext
private let message: EngineMessage private let message: EngineMessage
private let media: EngineMedia private let media: EngineMedia
private let imageNode: TransformImageNode private let imageView: TransformImageView
private var requestedImage: Bool = false private var requestedImage: Bool = false
private var disposable: Disposable? private var disposable: Disposable?
@ -25,11 +25,15 @@ private final class MediaPreviewNode: ASDisplayNode {
self.message = message self.message = message
self.media = media 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 { deinit {
@ -44,7 +48,7 @@ private final class MediaPreviewNode: ASDisplayNode {
if !self.requestedImage { if !self.requestedImage {
self.requestedImage = true 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) 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 { } else if case let .file(file) = self.media {
@ -53,13 +57,13 @@ private final class MediaPreviewNode: ASDisplayNode {
if !self.requestedImage { if !self.requestedImage {
self.requestedImage = true 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) 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() let makeLayout = self.imageView.asyncLayout()
self.imageNode.frame = CGRect(origin: CGPoint(), size: size) 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())) let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply() apply()
} }
@ -152,6 +156,13 @@ private final class ImageCache: Equatable {
var color: UInt32 var color: UInt32
} }
private struct Text: Hashable {
var fontSize: CGFloat
var isSemibold: Bool
var color: UInt32
var string: String
}
private var items: [AnyHashable: UIImage] = [:] private var items: [AnyHashable: UIImage] = [:]
func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage { func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage {
@ -169,6 +180,31 @@ private final class ImageCache: Equatable {
self.items[key] = image self.items[key] = image
return 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 { private final class DayComponent: Component {
@ -223,28 +259,28 @@ private final class DayComponent: Component {
} }
final class View: UIView { final class View: UIView {
private let buttonNode: HighlightableButtonNode private let button: HighlightableButton
private let highlightNode: ASImageNode private let highlightView: UIImageView
private let titleNode: ImmediateTextNode private let titleView: UIImageView
private var mediaPreviewNode: MediaPreviewNode? private var mediaPreviewView: MediaPreviewView?
private var action: (() -> Void)? private var action: (() -> Void)?
private var currentMedia: DayMedia? private var currentMedia: DayMedia?
init() { init() {
self.buttonNode = HighlightableButtonNode() self.button = HighlightableButton()
self.highlightNode = ASImageNode() self.highlightView = UIImageView()
self.titleNode = ImmediateTextNode() self.titleView = UIImageView()
super.init(frame: CGRect()) super.init(frame: CGRect())
self.buttonNode.addSubnode(self.highlightNode) self.button.addSubview(self.highlightView)
self.buttonNode.addSubnode(self.titleNode) 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) { 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 { func update(component: DayComponent, availableSize: CGSize, environment: Environment<ImageCache>, transition: Transition) -> CGSize {
self.action = component.action self.action = component.action
let shadowInset: CGFloat = 0.0
let diameter = min(availableSize.width, availableSize.height) 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] let imageCache = environment[ImageCache.self]
if component.media != nil { 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 { } 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 { } else {
self.highlightNode.image = nil self.highlightView.image = nil
} }
if self.currentMedia != component.media { if self.currentMedia != component.media {
self.currentMedia = component.media self.currentMedia = component.media
if let mediaPreviewNode = self.mediaPreviewNode { if let mediaPreviewView = self.mediaPreviewView {
self.mediaPreviewNode = nil self.mediaPreviewView = nil
mediaPreviewNode.removeFromSupernode() mediaPreviewView.removeFromSuperview()
} }
if let media = component.media { if let media = component.media {
let mediaPreviewNode = MediaPreviewNode(context: component.context, message: media.message, media: media.media) let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media)
self.mediaPreviewNode = mediaPreviewNode self.mediaPreviewView = mediaPreviewView
self.buttonNode.insertSubnode(mediaPreviewNode, belowSubnode: self.highlightNode) self.button.insertSubview(mediaPreviewView, belowSubview: self.highlightView)
} }
} }
let titleColor: UIColor let titleColor: UIColor
let titleFont: UIFont let titleFontSize: CGFloat
let titleFontIsSemibold: Bool
if component.isCurrent || component.media != nil { if component.isCurrent || component.media != nil {
titleColor = component.theme.list.itemCheckColors.foregroundColor titleColor = component.theme.list.itemCheckColors.foregroundColor
titleFont = Font.semibold(17.0) titleFontSize = 17.0
titleFontIsSemibold = true
} else if component.isEnabled { } else if component.isEnabled {
titleColor = component.theme.list.itemPrimaryTextColor titleColor = component.theme.list.itemPrimaryTextColor
titleFont = Font.regular(17.0) titleFontSize = 17.0
titleFontIsSemibold = false
} else { } else {
titleColor = component.theme.list.itemDisabledTextColor 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.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
self.buttonNode.isEnabled = component.isEnabled && component.media != nil
if let mediaPreviewNode = self.mediaPreviewNode { self.button.frame = CGRect(origin: CGPoint(), size: availableSize)
mediaPreviewNode.frame = CGRect(origin: CGPoint(), size: availableSize) self.button.isEnabled = component.isEnabled && component.media != nil
mediaPreviewNode.updateLayout(size: availableSize, synchronousLoads: false)
if let mediaPreviewView = self.mediaPreviewView {
mediaPreviewView.frame = contentFrame
mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false)
} }
return availableSize return availableSize
@ -382,6 +424,7 @@ private final class MonthComponent: CombinedComponent {
let weekdaySize: CGFloat = 46.0 let weekdaySize: CGFloat = 46.0
let weekdaySpacing: CGFloat = 6.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 weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0)
let title = title.update( let title = title.update(
@ -440,7 +483,7 @@ private final class MonthComponent: CombinedComponent {
environment: { environment: {
context.environment[ImageCache.self] context.environment[ImageCache.self]
}, },
availableSize: CGSize(width: weekdaySize, height: weekdaySize), availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize),
transition: .immediate 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) self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
//TODO:localize //TODO:localize
self.navigationItem.setTitle("Jump to Date", animated: false) self.navigationItem.setTitle("Calendar", animated: false)
} }
required public init(coder aDecoder: NSCoder) { required public init(coder aDecoder: NSCoder) {

View File

@ -2,6 +2,9 @@ import Foundation
import UIKit import UIKit
public final class ComponentHostView<EnvironmentType>: UIView { public final class ComponentHostView<EnvironmentType>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize?
private var currentSize: CGSize?
private var componentView: UIView? private var componentView: UIView?
private(set) var isUpdating: Bool = false 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 { 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 { 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 experimentalCompatibility(Bool)
case enableDebugDataDisplay(Bool) case enableDebugDataDisplay(Bool)
case acceleratedStickers(Bool) case acceleratedStickers(Bool)
case mockICE(Bool)
case playerEmbedding(Bool) case playerEmbedding(Bool)
case playlistPlayback(Bool) case playlistPlayback(Bool)
case voiceConference case voiceConference
@ -100,7 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue 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 return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec: case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue return DebugControllerSection.videoExperiments.rawValue
@ -169,14 +170,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 27 return 27
case .acceleratedStickers: case .acceleratedStickers:
return 29 return 29
case .playerEmbedding: case .mockICE:
return 30 return 30
case .playlistPlayback: case .playerEmbedding:
return 31 return 31
case .voiceConference: case .playlistPlayback:
return 32 return 32
case .voiceConference:
return 33
case let .preferredVideoCodec(index, _, _, _): case let .preferredVideoCodec(index, _, _, _):
return 33 + index return 34 + index
case .disableVideoAspectScaling: case .disableVideoAspectScaling:
return 100 return 100
case .enableVoipTcp: case .enableVoipTcp:
@ -749,6 +752,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).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): case let .playerEmbedding(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction 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(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay))
entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers))
entries.append(.mockICE(experimentalSettings.mockICE))
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
} }

View File

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

View File

@ -6,6 +6,7 @@ import UIKit
import TinyThumbnail import TinyThumbnail
import Display import Display
import FastBlur import FastBlur
import MozjpegBinding
private func generateBlurredThumbnail(image: UIImage) -> UIImage? { private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0) let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
@ -22,6 +23,84 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
return thumbnailContext.generateImage() 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 DirectMediaImageCache {
public final class GetMediaResult { public final class GetMediaResult {
public let image: UIImage? public let image: UIImage?
@ -65,16 +144,17 @@ public final class DirectMediaImageCache {
} }
|> take(1)).start(next: { data in |> take(1)).start(next: { data in
if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: dataValue) { 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 scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width))
let filledSize = image.size.aspectFilled(size) let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true)
let imageRect = CGRect(origin: CGPoint(x: (size.width - filledSize.width) / 2.0, y: (size.height - filledSize.height) / 2.0), size: filledSize) 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) 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)) if let scaledImage = storeImage(context: scaledContext, to: cachePath) {
subscriber.putNext(scaledImage) subscriber.putNext(scaledImage)
subscriber.putCompletion() subscriber.putCompletion()
}
} }
} }
}) })
@ -113,14 +193,14 @@ public final class DirectMediaImageCache {
} }
if let resource = resource { 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) return GetMediaResult(image: image, loadSignal: nil)
} }
var blurredImage: UIImage? 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 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) { if let blurredImageValue = generateBlurredThumbnail(image: image) {
blurredImage = blurredImageValue blurredImage = blurredImageValue
} }

View File

@ -22,7 +22,7 @@ private class TimerTargetWrapper: NSObject {
private let beginDelay: Double = 0.12 private let beginDelay: Double = 0.12
private func cancelParentGestures(view: UIView) { public func cancelParentGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers { if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers { for recognizer in gestureRecognizers {
recognizer.state = .failed recognizer.state = .failed
@ -31,6 +31,9 @@ private func cancelParentGestures(view: UIView) {
if let node = (view as? ListViewBackingView)?.target { if let node = (view as? ListViewBackingView)?.target {
node.cancelSelection() node.cancelSelection()
} }
if let node = view.asyncdisplaykit_node as? HighlightTrackingButtonNode {
node.highligthedChanged(false)
}
if let superview = view.superview { if let superview = view.superview {
cancelParentGestures(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 playbackStatusDisposable = MetaDisposable()
private let playbackStatus = Promise<MediaPlayerStatus>() private let playbackStatus = Promise<MediaPlayerStatus>()
private var downloadStatusIconNode: DownloadIconNode private var downloadStatusIconNode: DownloadIconNode?
private var linearProgressNode: LinearProgressNode? private var linearProgressNode: LinearProgressNode?
private var context: AccountContext? private var context: AccountContext?
@ -216,15 +216,19 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode() self.textNode = TextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false self.textNode.isUserInteractionEnabled = false
self.descriptionNode = TextNode() self.descriptionNode = TextNode()
self.descriptionNode.displaysAsynchronously = false
self.descriptionNode.isUserInteractionEnabled = false self.descriptionNode.isUserInteractionEnabled = false
self.descriptionProgressNode = ImmediateTextNode() self.descriptionProgressNode = ImmediateTextNode()
self.descriptionProgressNode.displaysAsynchronously = false
self.descriptionProgressNode.isUserInteractionEnabled = false self.descriptionProgressNode.isUserInteractionEnabled = false
self.descriptionProgressNode.maximumNumberOfLines = 1 self.descriptionProgressNode.maximumNumberOfLines = 1
@ -237,6 +241,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.extensionIconNode.displayWithoutProcessing = true self.extensionIconNode.displayWithoutProcessing = true
self.extensionIconText = TextNode() self.extensionIconText = TextNode()
self.extensionIconText.displaysAsynchronously = false
self.extensionIconText.isUserInteractionEnabled = false self.extensionIconText.isUserInteractionEnabled = false
self.iconImageNode = TransformImageNode() self.iconImageNode = TransformImageNode()
@ -246,8 +251,6 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
self.iconStatusNode.isUserInteractionEnabled = false self.iconStatusNode.isUserInteractionEnabled = false
self.downloadStatusIconNode = DownloadIconNode()
self.restrictionNode = ASDisplayNode() self.restrictionNode = ASDisplayNode()
self.restrictionNode.isHidden = true self.restrictionNode.isHidden = true
@ -276,6 +279,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
return return
} }
cancelParentGestures(view: strongSelf.view)
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture) 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 (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())) 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 return (nodeLayout, { animation in
if let strongSelf = self { if let strongSelf = self {
if strongSelf.downloadStatusIconNode == nil {
strongSelf.downloadStatusIconNode = DownloadIconNode(theme: item.presentationData.theme.theme)
}
let transition: ContainedViewLayoutTransition let transition: ContainedViewLayoutTransition
if animation.isAnimated { if animation.isAnimated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
@ -743,7 +752,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) 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 { if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
@ -855,7 +864,9 @@ 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 { if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls) let _ = strongSelf.fetchControls.swap(updatedFetchControls)
@ -982,6 +993,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
override public func updateSelectionState(animated: Bool) { override public func updateSelectionState(animated: Bool) {
} }
public func cancelPreviewGesture() {
self.containerNode.cancelGesture()
}
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let item = self.appliedItem else { guard let item = self.appliedItem else {
return return
@ -1025,11 +1040,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {}) linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
var animated = true var animated = true
if self.downloadStatusIconNode.supernode == nil { if let downloadStatusIconNode = self.downloadStatusIconNode {
animated = false if downloadStatusIconNode.supernode == nil {
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) animated = false
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
}
downloadStatusIconNode.enqueueState(.pause, animated: animated)
} }
self.downloadStatusIconNode.enqueueState(.pause, animated: animated)
case .Local: case .Local:
if let linearProgressNode = self.linearProgressNode { if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil self.linearProgressNode = nil
@ -1039,8 +1056,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
}) })
}) })
} }
if self.downloadStatusIconNode.supernode != nil { if let downloadStatusIconNode = self.downloadStatusIconNode {
self.downloadStatusIconNode.removeFromSupernode() if downloadStatusIconNode.supernode != nil {
downloadStatusIconNode.removeFromSupernode()
}
} }
case .Remote: case .Remote:
if let linearProgressNode = self.linearProgressNode { if let linearProgressNode = self.linearProgressNode {
@ -1049,12 +1068,14 @@ public final class ListMessageFileItemNode: ListMessageNode {
linearProgressNode?.removeFromSupernode() linearProgressNode?.removeFromSupernode()
}) })
} }
var animated = true if let downloadStatusIconNode = self.downloadStatusIconNode {
if self.downloadStatusIconNode.supernode == nil { var animated = true
animated = false if downloadStatusIconNode.supernode == nil {
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) animated = false
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
}
downloadStatusIconNode.enqueueState(.download, animated: animated)
} }
self.downloadStatusIconNode.enqueueState(.download, animated: animated)
} }
} else { } else {
if let linearProgressNode = self.linearProgressNode { if let linearProgressNode = self.linearProgressNode {
@ -1063,8 +1084,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
linearProgressNode?.removeFromSupernode() linearProgressNode?.removeFromSupernode()
}) })
} }
if self.downloadStatusIconNode.supernode != nil { if let downloadStatusIconNode = self.downloadStatusIconNode {
self.downloadStatusIconNode.removeFromSupernode() 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)) transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize))
} }
func activateMedia() { public func activateMedia() {
self.progressPressed() self.progressPressed()
} }
@ -1288,14 +1311,36 @@ private enum DownloadIconNodeState: Equatable {
case pause 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 let duration: Double = 0.3
private var iconState: DownloadIconNodeState = .download private var iconState: DownloadIconNodeState = .download
private var animationNode: ManagedAnimationNode?
init() { init(theme: PresentationTheme) {
super.init(size: CGSize(width: 18.0, height: 18.0)) self.customColor = theme.list.itemAccentColor
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) 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) { func enqueueState(_ state: DownloadIconNodeState, animated: Bool) {
@ -1303,6 +1348,19 @@ private final class DownloadIconNode: ManagedAnimationNode {
return 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 let previousState = self.iconState
self.iconState = state self.iconState = state
@ -1311,9 +1369,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
switch state { switch state {
case .download: case .download:
if animated { 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 { } 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: case .pause:
break break
@ -1322,9 +1380,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
switch state { switch state {
case .pause: case .pause:
if animated { 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 { } 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: case .download:
break break

View File

@ -48,7 +48,7 @@ public final class ListMessageItem: ListViewItem {
let chatLocation: ChatLocation let chatLocation: ChatLocation
let interaction: ListMessageItemInteraction let interaction: ListMessageItemInteraction
let message: Message let message: Message
let selection: ChatHistoryMessageSelection public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool let hintIsLink: Bool
let isGlobalSearchResult: Bool let isGlobalSearchResult: Bool

View File

@ -3,3 +3,4 @@
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage); NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size); 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/turbojpeg.h>
#import <mozjpeg/jpeglib.h> #import <mozjpeg/jpeglib.h>
#import <Accelerate/Accelerate.h>
static NSData *getHeaderPattern() { static NSData *getHeaderPattern() {
static NSData *value = nil; static NSData *value = nil;
@ -253,3 +254,80 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size)
return serializedData; 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/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow", "//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimationUI:AnimationUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -13,17 +13,32 @@ private let nullAction = NullActionClass()
public protocol SparseItemGridLayer: CALayer { public protocol SparseItemGridLayer: CALayer {
func update(size: CGSize) 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 { public protocol SparseItemGridBinding: AnyObject {
func createLayer() -> SparseItemGridLayer func createLayer() -> SparseItemGridLayer?
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer]) func createView() -> SparseItemGridView?
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], synchronous: Bool)
func unbindLayer(layer: SparseItemGridLayer) func unbindLayer(layer: SparseItemGridLayer)
func scrollerTextForTag(tag: Int32) -> String? func scrollerTextForTag(tag: Int32) -> String?
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
func onTap(item: SparseItemGrid.Item) func onTap(item: SparseItemGrid.Item)
func onTagTap() func onTagTap()
func didScroll() 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?) { 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 { private final class Shimmer {
public final class ShimmerLayer: CAGradientLayer { private var image: UIImage?
override public init() { private var colors: SparseItemGrid.ShimmerColors = SparseItemGrid.ShimmerColors(background: 0, foreground: 0)
super.init()
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) { if let image = self.image {
fatalError("init(coder:) has not been implemented") 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 return nullAction
} }
}
}
func update(size: CGSize) { public final class SparseItemGrid: ASDisplayNode {
self.endPoint = CGPoint(x: 0.0, y: size.height) 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 { private final class Viewport: ASDisplayNode, UIScrollViewDelegate {
final class VisibleItemLayer { final class VisibleItem: SparseItemGridDisplayItem {
let layer: SparseItemGridLayer let layer: SparseItemGridLayer?
let view: SparseItemGridView?
init(layer: SparseItemGridLayer) { init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
self.layer = layer 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 { final class Layout {
let containerLayout: ContainerLayout let containerLayout: ContainerLayout
let itemSize: CGFloat let itemSize: CGSize
let itemSpacing: CGFloat let itemSpacing: CGFloat
let lastItemSize: CGFloat let lastItemSize: CGFloat
let itemsPerRow: Int let itemsPerRow: Int
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) { init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
self.containerLayout = containerLayout 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 width = containerLayout.size.width
let baseItemWidth = floor(min(150.0, width / 3.0)) let baseItemWidth = floor(min(150.0, width / 3.0))
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
let itemsPerRow = floor(width / unclippedItemWidth) let itemsPerRow = floor(width / unclippedItemWidth)
self.itemsPerRow = Int(itemsPerRow) self.itemsPerRow = Int(itemsPerRow)
self.itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / 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 { func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow let row = index / self.itemsPerRow
let column = 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 { 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) { 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) 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 minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
@ -313,16 +418,22 @@ public final class SparseItemGrid: ASDisplayNode {
let zoomLevel: ZoomLevel let zoomLevel: ZoomLevel
private let scrollView: UIScrollView private let scrollView: UIScrollView
private let shimmer: Shimmer
var layout: Layout? var layout: Layout?
var items: Items? var items: Items?
var visibleItems: [AnyHashable: VisibleItemLayer] = [:] var visibleItems: [AnyHashable: VisibleItem] = [:]
var visiblePlaceholders: [ShimmerLayer] = [] var visiblePlaceholders: [Shimmer.Layer] = []
private var scrollingArea: SparseItemGridScrollingArea? private var scrollingArea: SparseItemGridScrollingArea?
private var currentScrollingTag: Int32?
private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void
private var ignoreScrolling: Bool = false 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) { init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void) {
self.zoomLevel = zoomLevel self.zoomLevel = zoomLevel
@ -338,6 +449,8 @@ public final class SparseItemGrid: ASDisplayNode {
self.scrollView.delaysContentTouches = false self.scrollView.delaysContentTouches = false
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = false
self.shimmer = Shimmer()
super.init() super.init()
self.anchorPoint = CGPoint() self.anchorPoint = CGPoint()
@ -351,13 +464,105 @@ public final class SparseItemGrid: ASDisplayNode {
self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel) self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel)
self.items = items 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) { @objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { 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) let localPoint = self.scrollView.convert(point, from: self.view)
for (id, visibleItem) in self.visibleItems { for (id, visibleItem) in self.visibleItems {
if visibleItem.layer.frame.contains(localPoint) { if visibleItem.frame.contains(localPoint) {
for item in items.items { for item in items.items {
if item.id == id { if item.id == id {
return item return item
@ -391,7 +596,7 @@ public final class SparseItemGrid: ASDisplayNode {
var closestItem: (CGFloat, AnyHashable)? var closestItem: (CGFloat, AnyHashable)?
for (id, visibleItem) in self.visibleItems { for (id, visibleItem) in self.visibleItems {
let itemCenter = visibleItem.layer.frame.center let itemCenter = visibleItem.frame.center
let distanceX = itemCenter.x - localPoint.x let distanceX = itemCenter.x - localPoint.x
let distanceY = itemCenter.y - localPoint.y let distanceY = itemCenter.y - localPoint.y
let distance2 = distanceX * distanceX + distanceY * distanceY 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) 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 { guard let layout = self.layout, let items = self.items else {
return return
} }
let contentHeight = layout.contentHeight(count: items.count) let contentHeight = layout.contentHeight(count: items.count)
let shimmerColors = items.itemBinding.getShimmerColors()
if resetScrolling { if resetScrolling {
if !self.scrollView.bounds.isEmpty { if !self.scrollView.bounds.isEmpty {
@ -494,49 +709,65 @@ public final class SparseItemGrid: ASDisplayNode {
var usedPlaceholderCount = 0 var usedPlaceholderCount = 0
if !items.items.isEmpty { if !items.items.isEmpty {
var bindItems: [Item] = [] var bindItems: [Item] = []
var bindLayers: [SparseItemGridLayer] = [] var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridLayer] = [] var updateLayers: [SparseItemGridDisplayItem] = []
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
for index in visibleRange.minIndex ... visibleRange.maxIndex { for index in visibleRange.minIndex ... visibleRange.maxIndex {
if let item = items.item(at: index) { if let item = items.item(at: index) {
let itemLayer: VisibleItemLayer let itemLayer: VisibleItem
if let current = self.visibleItems[item.id] { if let current = self.visibleItems[item.id] {
itemLayer = current itemLayer = current
updateLayers.append(itemLayer.layer) updateLayers.append(itemLayer)
} else { } else {
itemLayer = VisibleItemLayer(layer: items.itemBinding.createLayer()) itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
self.visibleItems[item.id] = itemLayer self.visibleItems[item.id] = itemLayer
bindItems.append(item) 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) validIds.insert(item.id)
itemLayer.layer.frame = layout.frame(at: index) itemLayer.frame = layout.frame(at: index)
} else { } else if layout.containerLayout.fixedItemHeight == nil {
let placeholderLayer: ShimmerLayer let placeholderLayer: Shimmer.Layer
if self.visiblePlaceholders.count > usedPlaceholderCount { if self.visiblePlaceholders.count > usedPlaceholderCount {
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount] placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
} else { } else {
placeholderLayer = ShimmerLayer() placeholderLayer = Shimmer.Layer()
self.scrollView.layer.addSublayer(placeholderLayer) self.scrollView.layer.addSublayer(placeholderLayer)
self.visiblePlaceholders.append(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 usedPlaceholderCount += 1
} }
} }
if !bindItems.isEmpty { if !bindItems.isEmpty {
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers) items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, synchronous: synchronous)
} }
for layer in updateLayers { for item in updateLayers {
layer.update(size: layer.bounds.size) 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 { for id in removeIds {
if let itemLayer = self.visibleItems.removeValue(forKey: id) { if let item = self.visibleItems.removeValue(forKey: id) {
items.itemBinding.unbindLayer(layer: itemLayer.layer) if let layer = item.layer {
itemLayer.layer.removeFromSuperlayer() 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 { guard let strongSelf = self else {
return nil return nil
} }
strongSelf.items?.itemBinding.onBeginFastScrolling()
return strongSelf.scrollView 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() self.updateScrollingArea()
} }
} }
@ -624,13 +868,20 @@ public final class SparseItemGrid: ASDisplayNode {
} }
if let scrollingArea = self.scrollingArea { 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( scrollingArea.update(
containerSize: layout.containerLayout.size, containerSize: layout.containerLayout.size,
containerInsets: layout.containerLayout.insets, containerInsets: layout.containerLayout.insets,
contentHeight: contentHeight, contentHeight: contentHeight,
contentOffset: self.scrollView.bounds.minY, contentOffset: self.scrollView.bounds.minY,
isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating, isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating,
dateString: tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } ?? "", dateString: dateString ?? "",
transition: .immediate transition: .immediate
) )
} }
@ -740,8 +991,10 @@ public final class SparseItemGrid: ASDisplayNode {
var insets: UIEdgeInsets var insets: UIEdgeInsets
var scrollIndicatorInsets: UIEdgeInsets var scrollIndicatorInsets: UIEdgeInsets
var lockScrollingAtTop: Bool var lockScrollingAtTop: Bool
var fixedItemHeight: CGFloat?
} }
private var tapRecognizer: UITapGestureRecognizer?
private var pinchRecognizer: UIPinchGestureRecognizer? private var pinchRecognizer: UIPinchGestureRecognizer?
private var containerLayout: ContainerLayout? private var containerLayout: ContainerLayout?
@ -754,6 +1007,13 @@ public final class SparseItemGrid: ASDisplayNode {
private var isLoadingHole: Bool = false private var isLoadingHole: Bool = false
private let loadingHoleDisposable = MetaDisposable() private let loadingHoleDisposable = MetaDisposable()
public var coveringInsetOffset: CGFloat {
if let currentViewport = self.currentViewport {
return currentViewport.coveringInsetOffset
}
return 0.0
}
override public init() { override public init() {
self.scrollingArea = SparseItemGridScrollingArea() self.scrollingArea = SparseItemGridScrollingArea()
@ -762,6 +1022,7 @@ public final class SparseItemGrid: ASDisplayNode {
self.clipsToBounds = true self.clipsToBounds = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.view.addGestureRecognizer(tapRecognizer) self.view.addGestureRecognizer(tapRecognizer)
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) 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) { 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) let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight)
self.containerLayout = containerLayout self.containerLayout = containerLayout
self.items = items self.items = items
self.scrollingArea.isHidden = lockScrollingAtTop self.scrollingArea.isHidden = lockScrollingAtTop
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
if self.currentViewport == nil { if self.currentViewport == nil {
let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
guard let strongSelf = self else { 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 { guard let currentViewport = self.currentViewport else {
return return
} }
for (_, itemLayer) in currentViewport.visibleItems { for (_, itemLayer) in currentViewport.visibleItems {
f(itemLayer.layer) f(itemLayer)
} }
} }
@ -1086,7 +1350,18 @@ public final class SparseItemGrid: ASDisplayNode {
currentViewport.scrollToItem(at: index) currentViewport.scrollToItem(at: index)
} }
public func scrollToTop() -> Bool {
guard let currentViewport = self.currentViewport else {
return false
}
return currentViewport.scrollToTop()
}
public func addToTransitionSurface(view: UIView) { public func addToTransitionSurface(view: UIView) {
self.view.insertSubview(view, belowSubview: self.scrollingArea.view) 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 AsyncDisplayKit
import ComponentFlow import ComponentFlow
import SwiftSignalKit 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 { private final class RoundedRectangle: Component {
let color: UIColor let color: UIColor
@ -134,7 +391,7 @@ private final class ShadowRoundedRectangle: Component {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(component.color.cgColor) 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))) 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) })?.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 dateIndicator: ComponentHostView<Empty>
private let lineIndicator: 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 indicatorPosition: CGFloat?
private var scrollIndicatorHeight: CGFloat? private var scrollIndicatorHeight: CGFloat?
@ -336,6 +598,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
private var activityTimer: SwiftSignalKit.Timer? private var activityTimer: SwiftSignalKit.Timer?
public var beginScrolling: (() -> UIScrollView?)? public var beginScrolling: (() -> UIScrollView?)?
public var setContentOffset: ((CGPoint) -> Void)?
public var openCurrentDate: (() -> Void)? public var openCurrentDate: (() -> Void)?
private var offsetBarTimer: SwiftSignalKit.Timer? private var offsetBarTimer: SwiftSignalKit.Timer?
@ -350,6 +613,20 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
} }
private var projectionData: ProjectionData? 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() { override public init() {
self.dateIndicator = ComponentHostView<Empty>() self.dateIndicator = ComponentHostView<Empty>()
self.lineIndicator = ComponentHostView<Empty>() self.lineIndicator = ComponentHostView<Empty>()
@ -399,10 +676,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
if let scrollView = strongSelf.beginScrolling?() { if let scrollView = strongSelf.beginScrolling?() {
strongSelf.draggingScrollView = scrollView strongSelf.draggingScrollView = scrollView
strongSelf.scrollingInitialOffset = scrollView.contentOffset.y 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 ended: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -424,7 +701,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
strongSelf.updateLineIndicator(transition: transition) strongSelf.updateLineIndicator(transition: transition)
strongSelf.updateActivityTimer() strongSelf.updateActivityTimer(isScrolling: false)
}, },
moved: { [weak self] relativeOffset in moved: { [weak self] relativeOffset in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -454,7 +731,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
offset = scrollView.contentSize.height - scrollView.bounds.height 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 _ = scrollView
let _ = projectionData let _ = projectionData
} }
@ -473,6 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
self.updateLineIndicator(transition: transition) self.updateLineIndicator(transition: transition)
} }
func feedbackTap() {
self.hapticFeedback.tap()
}
public func update( public func update(
containerSize: CGSize, containerSize: CGSize,
containerInsets: UIEdgeInsets, containerInsets: UIEdgeInsets,
@ -482,8 +763,15 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
dateString: String, dateString: String,
transition: ContainedViewLayoutTransition transition: ContainedViewLayoutTransition
) { ) {
self.containerSize = containerSize
if self.dateIndicator.alpha.isZero {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint())
}
if isScrolling { if isScrolling {
self.updateActivityTimer() self.updateActivityTimer(isScrolling: true)
} }
let indicatorSize = self.dateIndicator.update( let indicatorSize = self.dateIndicator.update(
@ -508,18 +796,18 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
} }
let indicatorVerticalInset: CGFloat = 3.0 let indicatorVerticalInset: CGFloat = 3.0
let topIndicatorInset: CGFloat = indicatorVerticalInset let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom 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 indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height)))
let indicatorTopPosition = topIndicatorInset let indicatorTopPosition = topIndicatorInset
let indicatorBottomPosition = containerSize.height - bottomIndicatorInset - scrollIndicatorHeight let indicatorBottomPosition = containerSize.height - bottomIndicatorInset - scrollIndicatorHeight
let dateIndicatorTopPosition = topIndicatorInset let dateIndicatorTopPosition = topIndicatorInset + 4.0
let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - indicatorSize.height let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - 4.0 - indicatorSize.height
self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction
self.scrollIndicatorHeight = scrollIndicatorHeight 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)) transition.updateFrame(view: self.dateIndicator, frame: CGRect(origin: CGPoint(x: containerSize.width - 12.0 - indicatorSize.width, y: dateIndicatorPosition), size: indicatorSize))
if isScrolling { if isScrolling {
self.dateIndicator.alpha = 1.0 let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
self.lineIndicator.alpha = 1.0 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) self.updateLineIndicator(transition: transition)
if isScrolling {
self.displayTooltipOnFirstScroll()
}
} }
private func updateLineIndicator(transition: ContainedViewLayoutTransition) { private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
@ -547,7 +842,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
return 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 let mappedTransition: Transition
switch transition { switch transition {
case .immediate: 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)) 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() self.activityTimer?.invalidate()
if self.isDragging { if self.isDragging {
@ -582,11 +877,71 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0) transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
transition.updateAlpha(layer: strongSelf.lineIndicator.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()) }, queue: .mainQueue())
self.activityTimer?.start() 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? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.dateIndicator.alpha <= 0.01 { if self.dateIndicator.alpha <= 0.01 {
return nil return nil

View File

@ -683,7 +683,9 @@ public final class PresentationCallImpl: PresentationCall {
if let _ = audioSessionControl, !wasActive || previousControl == nil { if let _ = audioSessionControl, !wasActive || previousControl == nil {
let logName = "\(id.id)_\(id.accessHash)" let logName = "\(id.id)_\(id.accessHash)"
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 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: 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 self.ongoingContext = ongoingContext
ongoingContext.setIsMuted(self.isMutedValue) ongoingContext.setIsMuted(self.isMutedValue)
if let requestedVideoAspect = self.requestedVideoAspect { if let requestedVideoAspect = self.requestedVideoAspect {

View File

@ -232,6 +232,11 @@ private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessio
public struct CallSessionConnectionSet { public struct CallSessionConnectionSet {
public let primary: CallSessionConnection public let primary: CallSessionConnection
public let alternatives: [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 { private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.PhoneConnection]) -> CallSessionConnectionSet {

View File

@ -160,6 +160,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case chatSpecificThemeLightPreviewTip = 26 case chatSpecificThemeLightPreviewTip = 26
case chatSpecificThemeDarkPreviewTip = 27 case chatSpecificThemeDarkPreviewTip = 27
case interactiveEmojiSyncTip = 28 case interactiveEmojiSyncTip = 28
case sharedMediaScrollingTooltip = 29
case sharedMediaFastScrollingTooltip = 30
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -324,6 +326,14 @@ private struct ApplicationSpecificNoticeKeys {
static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey { static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: peerInviteRequestsNamespace), key: noticeKey(peerId: peerId, key: 0)) 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 { public struct ApplicationSpecificNotice {
@ -894,6 +904,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> { public static func dismissedTrendingStickerPacks(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<[Int64]?, NoError> {
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks()) return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks())
|> map { view -> [Int64]? in |> map { view -> [Int64]? in

View File

@ -679,27 +679,9 @@ public struct PresentationResourcesChat {
}) })
} }
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? { public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme, generate: () -> UIImage?) -> UIImage? {
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { _ in
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in return generate()
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()
})
}) })
} }

View File

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

View File

@ -922,9 +922,34 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView
let displayRange = strongSelf.displayedItemRange let displayRange = strongSelf.displayedItemRange
if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange { if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange {
let firstEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.firstIndex] var anchorIndex: MessageIndex?
loop: for index in visibleRange.firstIndex ..< filteredEntries.count {
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) 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 { } else {
if let subject = subject, case let .message(messageId, highlight, _) = subject { 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) strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)

View File

@ -82,6 +82,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
return .single(nil) return .single(nil)
} }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
private var disposable: Disposable? private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) { init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
@ -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 let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData) 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 self.listNode.scrollEnabled = !isScrollingLockedAtTop

View File

@ -24,7 +24,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let listNode: ChatHistoryListNode 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 let ready = Promise<Bool>()
private var didSetReady: Bool = false private var didSetReady: Bool = false
@ -55,6 +55,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
self.statusPromise.get() 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) { init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
@ -129,8 +134,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
strongSelf.playlistStateAndType = nil strongSelf.playlistStateAndType = nil
} }
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams { if let (size, topInset, 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)) 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) { 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, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
var topPanelHeight: CGFloat = 0.0 var topPanelHeight: CGFloat = 0.0
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType { 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)) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) 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 { if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() { switch self.listNode.visibleContentOffset() {
case let .known(value) where value <= CGFloat.ulpOfOne: case let .known(value) where value <= CGFloat.ulpOfOne:

View File

@ -125,6 +125,11 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
return .single(nil) return .single(nil)
} }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
private var disposable: Disposable? private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) { init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
@ -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 let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop) self.currentParams = (size, isScrollingLockedAtTop)
self.presentationDataPromise.set(.single(presentationData)) 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) 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 self.listNode.scrollEnabled = !isScrollingLockedAtTop

View File

@ -1044,7 +1044,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable {
} }
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] private(set) var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = [] private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = []
@ -1666,6 +1666,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleNodeContainer: ASDisplayNode let subtitleNodeContainer: ASDisplayNode
let subtitleNodeRawContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode
let subtitleNode: MultiScaleTextNode let subtitleNode: MultiScaleTextNode
let panelSubtitleNode: MultiScaleTextNode
let usernameNodeContainer: ASDisplayNode let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode let usernameNode: MultiScaleTextNode
@ -1721,6 +1722,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.subtitleNode.displaysAsynchronously = false self.subtitleNode.displaysAsynchronously = false
self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.panelSubtitleNode.displaysAsynchronously = false
self.usernameNodeContainer = ASDisplayNode() self.usernameNodeContainer = ASDisplayNode()
self.usernameNodeRawContainer = ASDisplayNode() self.usernameNodeRawContainer = ASDisplayNode()
self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
@ -1770,6 +1774,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.titleNodeContainer.addSubnode(self.titleNode) self.titleNodeContainer.addSubnode(self.titleNode)
self.regularContentNode.addSubnode(self.titleNodeContainer) self.regularContentNode.addSubnode(self.titleNodeContainer)
self.subtitleNodeContainer.addSubnode(self.subtitleNode) self.subtitleNodeContainer.addSubnode(self.subtitleNode)
self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode)
self.regularContentNode.addSubnode(self.subtitleNodeContainer) self.regularContentNode.addSubnode(self.subtitleNodeContainer)
self.regularContentNode.addSubnode(self.subtitleNodeRawContainer) self.regularContentNode.addSubnode(self.subtitleNodeRawContainer)
self.usernameNodeContainer.addSubnode(self.usernameNode) self.usernameNodeContainer.addSubnode(self.usernameNode)
@ -1899,7 +1904,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
var initializedCredibilityIcon = false 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.state = state
self.peer = peer self.peer = peer
self.avatarListNode.listContainerNode.peer = peer self.avatarListNode.listContainerNode.peer = peer
@ -2019,6 +2024,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var isVerified = false var isVerified = false
let titleString: NSAttributedString let titleString: NSAttributedString
let subtitleString: NSAttributedString let subtitleString: NSAttributedString
var panelSubtitleString: NSAttributedString?
let usernameString: NSAttributedString let usernameString: NSAttributedString
if let peer = peer, peer.isVerified { if let peer = peer, peer.isVerified {
isVerified = true isVerified = true
@ -2063,6 +2069,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor) 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) 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 { } else {
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) 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) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
@ -2090,6 +2106,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.subtitleNode.accessibilityLabel = subtitleString.string 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: [ let usernameNodeLayout = self.usernameNode.updateLayout(states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) TitleNodeStateExpanded: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
@ -2102,6 +2124,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size
let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size
let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size
let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size
let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size
if let image = self.titleCredibilityIconNode.image { if let image = self.titleCredibilityIconNode.image {
@ -2151,17 +2174,53 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset 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: [ self.titleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], transition: transition) ], 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: [ self.subtitleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], alpha: subtitleAlpha, transition: transition) ], 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: [ self.usernameNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.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) 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 self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) 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.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) 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 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.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.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale)

View File

@ -15,8 +15,10 @@ protocol PeerInfoPaneNode: ASDisplayNode {
var parentController: ViewController? { get set } var parentController: ViewController? { get set }
var status: Signal<PeerInfoStatusData?, NoError> { get } 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 scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat) func transferVelocity(_ velocity: CGFloat)
func cancelPreviewGestures() func cancelPreviewGestures()
@ -32,21 +34,21 @@ final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey let key: PeerInfoPaneKey
let node: PeerInfoPaneNode let node: PeerInfoPaneNode
var isAnimatingOut: Bool = false 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) { init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
self.key = key self.key = key
self.node = node self.node = node
} }
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) {
if let (currentSize, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
return return
} }
} }
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) self.appliedParams = (size, topInset, 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.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() paneDidScroll()
} }
case .files: 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: case .links:
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage) paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage)
case .voice: 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: 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: case .gifs:
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs) let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
paneNode = visualPaneNode paneNode = visualPaneNode
@ -446,6 +454,7 @@ private final class PeerInfoPendingPane {
final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private let isMediaOnly: Bool
weak var parentController: ViewController? weak var parentController: ViewController?
@ -470,6 +479,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
} }
} }
private let currentPaneStatusPromise = Promise<PeerInfoStatusData?>(nil)
var currentPaneStatus: Signal<PeerInfoStatusData?, NoError> {
return self.currentPaneStatusPromise.get()
}
private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:]
private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:]
@ -490,10 +504,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
private var currentAvailablePanes: [PeerInfoPaneKey]? private var currentAvailablePanes: [PeerInfoPaneKey]?
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? 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.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.peerId = peerId self.peerId = peerId
self.isMediaOnly = isMediaOnly
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true 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.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.currentPaneUpdated?(true)
strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil))
} }
} else if strongSelf.pendingSwitchToPaneKey != key { } else if strongSelf.pendingSwitchToPaneKey != key {
strongSelf.pendingSwitchToPaneKey = key strongSelf.pendingSwitchToPaneKey = key
@ -634,6 +651,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.transitionFraction = 0.0 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.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.currentPaneUpdated?(false)
self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
} }
default: default:
break break
@ -675,6 +694,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.currentAvailablePanes = availablePanes self.currentAvailablePanes = availablePanes
let previousCurrentPaneKey = self.currentPaneKey let previousCurrentPaneKey = self.currentPaneKey
var updateCurrentPaneStatus = false
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
var nextCandidatePaneKey: PeerInfoPaneKey? var nextCandidatePaneKey: PeerInfoPaneKey?
@ -715,15 +735,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne
let tabsHeight: CGFloat = 48.0 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))) let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
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))
var visiblePaneIndices: [Int] = [] var visiblePaneIndices: [Int] = []
var requiredPendingKeys: [PeerInfoPaneKey] = [] var requiredPendingKeys: [PeerInfoPaneKey] = []
@ -794,14 +810,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
) )
self.pendingPanes[key] = pane self.pendingPanes[key] = pane
pane.pane.node.frame = paneFrame 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 leftScope = true
} }
} }
for (key, pane) in self.pendingPanes { for (key, pane) in self.pendingPanes {
pane.pane.node.frame = paneFrame 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 { if pane.isReady {
self.pendingPanes.removeValue(forKey: key) self.pendingPanes.removeValue(forKey: key)
@ -818,6 +843,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.pendingSwitchToPaneKey = nil self.pendingSwitchToPaneKey = nil
previousPaneKey = self.currentPaneKey previousPaneKey = self.currentPaneKey
self.currentPaneKey = pendingSwitchToPaneKey self.currentPaneKey = pendingSwitchToPaneKey
updateCurrentPaneStatus = true
updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey)
if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex {
if updatedCurrentIndex < previousIndex { if updatedCurrentIndex < previousIndex {
@ -834,7 +860,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
var paneWasAdded = false var paneWasAdded = false
if pane.node.supernode == nil { if pane.node.supernode == nil {
self.addSubnode(pane.node) self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode)
paneWasAdded = true paneWasAdded = true
} }
let indexOffset = CGFloat(index - updatedCurrentIndex) let indexOffset = CGFloat(index - updatedCurrentIndex)
@ -878,13 +904,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
paneCompletion() 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 })") 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)))
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight)))
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String let title: String
switch key { switch key {
@ -911,7 +953,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
for (_, pane) in self.pendingPanes { for (_, pane) in self.pendingPanes {
let paneTransition: ContainedViewLayoutTransition = .immediate let paneTransition: ContainedViewLayoutTransition = .immediate
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) 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 !self.didSetIsReady && data != nil {
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { 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 { if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey {
self.currentPaneUpdated?(true) 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 PeerInfoAvatarListNode
import PasswordSetupUI import PasswordSetupUI
import CalendarMessageScreen import CalendarMessageScreen
import TooltipUI
protocol PeerInfoScreenItem: AnyObject { protocol PeerInfoScreenItem: AnyObject {
var id: AnyHashable { get } var id: AnyHashable { get }
@ -1617,7 +1618,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.scrollNode.canCancelAllTouchesInViews = true self.scrollNode.canCancelAllTouchesInViews = true
self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isSettings: isSettings) 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() super.init()
@ -2267,12 +2268,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return 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 let (layout, navigationHeight) = strongSelf.validLayout {
if strongSelf.headerNode.isAvatarExpanded { if strongSelf.headerNode.isAvatarExpanded {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
@ -2292,6 +2287,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
} }
self.customStatusPromise.set(self.paneContainerNode.currentPaneStatus)
self.paneContainerNode.requestExpandTabs = { [weak self] in self.paneContainerNode.requestExpandTabs = { [weak self] in
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else { guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
return false return false
@ -6010,6 +6007,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
private weak var mediaGalleryContextMenu: ContextController? 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) { private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) {
guard let controller = self.controller else { guard let controller = self.controller else {
return return
@ -6027,12 +6038,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
if !"".isEmpty { if !"".isEmpty {
canZoom = false canZoom = false
} }
/*if isZoomIn { return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "Zoom Out", textColor: canZoom ? .primary : .disabled, icon: { theme in
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 generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) 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 }, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { 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 updatedContentType = .photo
case .video: case .video:
updatedContentType = .photoOrVideo updatedContentType = .photoOrVideo
case .gifs: default:
updatedContentType = .gifs updatedContentType = pane.contentType
} }
pane.updateContentType(contentType: updatedContentType) pane.updateContentType(contentType: updatedContentType)
}))) })))
@ -6118,8 +6124,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
updatedContentType = .photoOrVideo updatedContentType = .photoOrVideo
case .video: case .video:
updatedContentType = .video updatedContentType = .video
case .gifs: default:
updatedContentType = .gifs updatedContentType = pane.contentType
} }
pane.updateContentType(contentType: updatedContentType) pane.updateContentType(contentType: updatedContentType)
}))) })))
@ -6187,7 +6193,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
var contentHeight: CGFloat = 0.0 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)) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))
if additive { if additive {
transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame)
@ -6436,7 +6442,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
if let (layout, navigationHeight) = self.validLayout { if let (layout, navigationHeight) = self.validLayout {
if !additive { 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 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) self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction)
var topHeight = topNavigationBar.backgroundNode.bounds.height var topHeight = topNavigationBar.backgroundNode.bounds.height
if let (layout, _) = self.screenNode.validLayout { 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 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 experimentalCompatibility: Bool
public var enableDebugDataDisplay: Bool public var enableDebugDataDisplay: Bool
public var acceleratedStickers: Bool public var acceleratedStickers: Bool
public var mockICE: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -34,7 +35,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
enableVoipTcp: false, enableVoipTcp: false,
experimentalCompatibility: false, experimentalCompatibility: false,
enableDebugDataDisplay: false, enableDebugDataDisplay: false,
acceleratedStickers: false acceleratedStickers: false,
mockICE: false
) )
} }
@ -52,7 +54,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
enableVoipTcp: Bool, enableVoipTcp: Bool,
experimentalCompatibility: Bool, experimentalCompatibility: Bool,
enableDebugDataDisplay: Bool, enableDebugDataDisplay: Bool,
acceleratedStickers: Bool acceleratedStickers: Bool,
mockICE: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -68,6 +71,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.experimentalCompatibility = experimentalCompatibility self.experimentalCompatibility = experimentalCompatibility
self.enableDebugDataDisplay = enableDebugDataDisplay self.enableDebugDataDisplay = enableDebugDataDisplay
self.acceleratedStickers = acceleratedStickers self.acceleratedStickers = acceleratedStickers
self.mockICE = mockICE
} }
public init(from decoder: Decoder) throws { 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.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0
self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 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.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 { 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.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility")
try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") 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.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers")
try container.encode((self.mockICE ? 1 : 0) as Int32, forKey: "mockICE")
} }
} }