mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
17229c7f3d
@ -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
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user