mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Calendar improvements
This commit is contained in:
parent
b1d4e7f259
commit
a8940ebf68
@ -453,18 +453,7 @@ private func formSupportApplePay(_ paymentForm: BotPaymentForm) -> Bool {
|
|||||||
guard let nativeProvider = paymentForm.nativeProvider else {
|
guard let nativeProvider = paymentForm.nativeProvider else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let applePayProviders = Set<String>([
|
|
||||||
"stripe",
|
|
||||||
"sberbank",
|
|
||||||
"yandex",
|
|
||||||
"privatbank",
|
|
||||||
"tranzzo",
|
|
||||||
"paymaster",
|
|
||||||
"smartglocal",
|
|
||||||
])
|
|
||||||
if !applePayProviders.contains(nativeProvider.name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else {
|
guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
26
submodules/CalendarMessageScreen/BUILD
Normal file
26
submodules/CalendarMessageScreen/BUILD
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "CalendarMessageScreen",
|
||||||
|
module_name = "CalendarMessageScreen",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
|
"//submodules/Postbox:Postbox",
|
||||||
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
|
"//submodules/PhotoResources:PhotoResources",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,845 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentFlow
|
||||||
|
import PhotoResources
|
||||||
|
|
||||||
|
private final class MediaPreviewNode: ASDisplayNode {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let message: EngineMessage
|
||||||
|
private let media: EngineMedia
|
||||||
|
|
||||||
|
private let imageNode: TransformImageNode
|
||||||
|
|
||||||
|
private var requestedImage: Bool = false
|
||||||
|
private var disposable: Disposable?
|
||||||
|
|
||||||
|
init(context: AccountContext, message: EngineMessage, media: EngineMedia) {
|
||||||
|
self.context = context
|
||||||
|
self.message = message
|
||||||
|
self.media = media
|
||||||
|
|
||||||
|
self.imageNode = TransformImageNode()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.imageNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, synchronousLoads: Bool) {
|
||||||
|
var dimensions = CGSize(width: 100.0, height: 100.0)
|
||||||
|
if case let .image(image) = self.media {
|
||||||
|
if let largest = largestImageRepresentation(image.representations) {
|
||||||
|
dimensions = largest.dimensions.cgSize
|
||||||
|
if !self.requestedImage {
|
||||||
|
self.requestedImage = true
|
||||||
|
let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads)
|
||||||
|
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if case let .file(file) = self.media {
|
||||||
|
if let mediaDimensions = file.dimensions {
|
||||||
|
dimensions = mediaDimensions.cgSize
|
||||||
|
if !self.requestedImage {
|
||||||
|
self.requestedImage = true
|
||||||
|
let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true)
|
||||||
|
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let makeLayout = self.imageNode.asyncLayout()
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func monthName(index: Int, strings: PresentationStrings) -> String {
|
||||||
|
switch index {
|
||||||
|
case 0:
|
||||||
|
return strings.Month_GenJanuary
|
||||||
|
case 1:
|
||||||
|
return strings.Month_GenFebruary
|
||||||
|
case 2:
|
||||||
|
return strings.Month_GenMarch
|
||||||
|
case 3:
|
||||||
|
return strings.Month_GenApril
|
||||||
|
case 4:
|
||||||
|
return strings.Month_GenMay
|
||||||
|
case 5:
|
||||||
|
return strings.Month_GenJune
|
||||||
|
case 6:
|
||||||
|
return strings.Month_GenJuly
|
||||||
|
case 7:
|
||||||
|
return strings.Month_GenAugust
|
||||||
|
case 8:
|
||||||
|
return strings.Month_GenSeptember
|
||||||
|
case 9:
|
||||||
|
return strings.Month_GenOctober
|
||||||
|
case 10:
|
||||||
|
return strings.Month_GenNovember
|
||||||
|
case 11:
|
||||||
|
return strings.Month_GenDecember
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dayName(index: Int, strings: PresentationStrings) -> String {
|
||||||
|
let _ = strings
|
||||||
|
//TODO:localize
|
||||||
|
|
||||||
|
switch index {
|
||||||
|
case 0:
|
||||||
|
return "M"
|
||||||
|
case 1:
|
||||||
|
return "T"
|
||||||
|
case 2:
|
||||||
|
return "W"
|
||||||
|
case 3:
|
||||||
|
return "T"
|
||||||
|
case 4:
|
||||||
|
return "F"
|
||||||
|
case 5:
|
||||||
|
return "S"
|
||||||
|
case 6:
|
||||||
|
return "S"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Scroller: UIScrollView {
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||||
|
self.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DayComponent: Component {
|
||||||
|
let title: String
|
||||||
|
let isCurrent: Bool
|
||||||
|
let isEnabled: Bool
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let context: AccountContext
|
||||||
|
let media: DayMedia?
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
isCurrent: Bool,
|
||||||
|
isEnabled: Bool,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
context: AccountContext,
|
||||||
|
media: DayMedia?,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.isCurrent = isCurrent
|
||||||
|
self.isEnabled = isEnabled
|
||||||
|
self.theme = theme
|
||||||
|
self.context = context
|
||||||
|
self.media = media
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: DayComponent, rhs: DayComponent) -> Bool {
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isCurrent != rhs.isCurrent {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isEnabled != rhs.isEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.media != rhs.media {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let buttonNode: HighlightableButtonNode
|
||||||
|
|
||||||
|
private let highlightNode: ASImageNode
|
||||||
|
private let titleNode: ImmediateTextNode
|
||||||
|
private var mediaPreviewNode: MediaPreviewNode?
|
||||||
|
|
||||||
|
private var currentTheme: PresentationTheme?
|
||||||
|
private var currentDiameter: CGFloat?
|
||||||
|
private var currentIsCurrent: Bool?
|
||||||
|
private var action: (() -> Void)?
|
||||||
|
private var currentMedia: DayMedia?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.buttonNode = HighlightableButtonNode()
|
||||||
|
self.highlightNode = ASImageNode()
|
||||||
|
self.titleNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.buttonNode.addSubnode(self.highlightNode)
|
||||||
|
self.buttonNode.addSubnode(self.titleNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.buttonNode)
|
||||||
|
|
||||||
|
self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
self.action?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: DayComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
self.action = component.action
|
||||||
|
|
||||||
|
let shadowInset: CGFloat = 0.0
|
||||||
|
let diameter = min(availableSize.width, availableSize.height)
|
||||||
|
|
||||||
|
var updated = false
|
||||||
|
if self.currentTheme !== component.theme || self.currentIsCurrent != component.isCurrent {
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.currentDiameter != diameter || updated {
|
||||||
|
self.currentDiameter = diameter
|
||||||
|
self.currentTheme = component.theme
|
||||||
|
self.currentIsCurrent = component.isCurrent
|
||||||
|
|
||||||
|
if component.isCurrent || component.media != nil {
|
||||||
|
self.highlightNode.image = generateImage(CGSize(width: diameter + shadowInset * 2.0, height: diameter + shadowInset * 2.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
if component.media != nil {
|
||||||
|
context.setFillColor(UIColor(white: 0.0, alpha: 0.2).cgColor)
|
||||||
|
} else {
|
||||||
|
context.setFillColor(component.theme.list.itemAccentColor.cgColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0)))
|
||||||
|
})?.stretchableImage(withLeftCapWidth: Int(diameter + shadowInset * 2.0) / 2, topCapHeight: Int(diameter + shadowInset * 2.0) / 2)
|
||||||
|
} else {
|
||||||
|
self.highlightNode.image = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.currentMedia != component.media {
|
||||||
|
if let mediaPreviewNode = self.mediaPreviewNode {
|
||||||
|
self.mediaPreviewNode = nil
|
||||||
|
mediaPreviewNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let media = component.media {
|
||||||
|
let mediaPreviewNode = MediaPreviewNode(context: component.context, message: media.message, media: media.media)
|
||||||
|
self.mediaPreviewNode = mediaPreviewNode
|
||||||
|
self.buttonNode.insertSubnode(mediaPreviewNode, belowSubnode: self.highlightNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleColor: UIColor
|
||||||
|
let titleFont: UIFont
|
||||||
|
if component.isCurrent || component.media != nil {
|
||||||
|
titleColor = component.theme.list.itemCheckColors.foregroundColor
|
||||||
|
titleFont = Font.semibold(17.0)
|
||||||
|
} else if component.isEnabled {
|
||||||
|
titleColor = component.theme.list.itemPrimaryTextColor
|
||||||
|
titleFont = Font.regular(17.0)
|
||||||
|
} else {
|
||||||
|
titleColor = component.theme.list.itemDisabledTextColor
|
||||||
|
titleFont = Font.regular(17.0)
|
||||||
|
}
|
||||||
|
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)))
|
||||||
|
|
||||||
|
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
|
||||||
|
self.buttonNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||||
|
self.buttonNode.isEnabled = component.isEnabled && component.media != nil
|
||||||
|
|
||||||
|
if let mediaPreviewNode = self.mediaPreviewNode {
|
||||||
|
mediaPreviewNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||||
|
mediaPreviewNode.updateLayout(size: availableSize, synchronousLoads: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MonthComponent: CombinedComponent {
|
||||||
|
let context: AccountContext
|
||||||
|
let model: MonthModel
|
||||||
|
let foregroundColor: UIColor
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let navigateToDay: (Int32) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
model: MonthModel,
|
||||||
|
foregroundColor: UIColor,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
navigateToDay: @escaping (Int32) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.model = model
|
||||||
|
self.foregroundColor = foregroundColor
|
||||||
|
self.strings = strings
|
||||||
|
self.theme = theme
|
||||||
|
self.navigateToDay = navigateToDay
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: MonthComponent, rhs: MonthComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.model != rhs.model {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.foregroundColor != rhs.foregroundColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let title = Child(Text.self)
|
||||||
|
let weekdayTitles = ChildMap(environment: Empty.self, keyedBy: Int.self)
|
||||||
|
let days = ChildMap(environment: Empty.self, keyedBy: Int.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let sideInset: CGFloat = 14.0
|
||||||
|
let titleWeekdaysSpacing: CGFloat = 18.0
|
||||||
|
let weekdayDaySpacing: CGFloat = 14.0
|
||||||
|
let weekdaySize: CGFloat = 46.0
|
||||||
|
let weekdaySpacing: CGFloat = 6.0
|
||||||
|
|
||||||
|
let weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0)
|
||||||
|
|
||||||
|
let title = title.update(
|
||||||
|
component: Text(
|
||||||
|
text: "\(monthName(index: context.component.model.index - 1, strings: context.component.strings)) \(context.component.model.year)",
|
||||||
|
font: Font.semibold(17.0),
|
||||||
|
color: .black
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
let updatedWeekdayTitles = (0 ..< 7).map { index in
|
||||||
|
return weekdayTitles[index].update(
|
||||||
|
component: AnyComponent(Text(
|
||||||
|
text: dayName(index: index, strings: context.component.strings),
|
||||||
|
font: Font.regular(10.0),
|
||||||
|
color: .black
|
||||||
|
)),
|
||||||
|
availableSize: CGSize(width: 100.0, height: 100.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedDays = (0 ..< context.component.model.numberOfDays).map { index -> _UpdatedChildComponent in
|
||||||
|
let dayOfMonth = index + 1
|
||||||
|
let isCurrent = context.component.model.currentYear == context.component.model.year && context.component.model.currentMonth == context.component.model.index && context.component.model.currentDayOfMonth == dayOfMonth
|
||||||
|
var isEnabled = true
|
||||||
|
if context.component.model.currentYear == context.component.model.year {
|
||||||
|
if context.component.model.currentMonth == context.component.model.index {
|
||||||
|
if dayOfMonth > context.component.model.currentDayOfMonth {
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
} else if context.component.model.index > context.component.model.currentMonth {
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
} else if context.component.model.year > context.component.model.currentYear {
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(index)
|
||||||
|
let navigateToDay = context.component.navigateToDay
|
||||||
|
|
||||||
|
return days[index].update(
|
||||||
|
component: AnyComponent(DayComponent(
|
||||||
|
title: "\(dayOfMonth)",
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
isEnabled: isEnabled,
|
||||||
|
theme: context.component.theme,
|
||||||
|
context: context.component.context,
|
||||||
|
media: context.component.model.mediaByDay[index],
|
||||||
|
action: {
|
||||||
|
navigateToDay(dayTimestamp)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
availableSize: CGSize(width: weekdaySize, height: weekdaySize),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - title.size.width) / 2.0), y: 0.0), size: title.size)
|
||||||
|
|
||||||
|
context.add(title
|
||||||
|
.position(CGPoint(x: titleFrame.midX, y: titleFrame.midY))
|
||||||
|
)
|
||||||
|
|
||||||
|
let baseWeekdayTitleY = titleFrame.maxY + titleWeekdaysSpacing
|
||||||
|
var maxWeekdayY = baseWeekdayTitleY
|
||||||
|
|
||||||
|
for i in 0 ..< updatedWeekdayTitles.count {
|
||||||
|
let weekdaySize = updatedWeekdayTitles[i].size
|
||||||
|
let weekdayFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * weekdayWidth + floor((weekdayWidth - weekdaySize.width) / 2.0), y: baseWeekdayTitleY), size: weekdaySize)
|
||||||
|
maxWeekdayY = max(maxWeekdayY, weekdayFrame.maxY)
|
||||||
|
context.add(updatedWeekdayTitles[i]
|
||||||
|
.position(CGPoint(x: weekdayFrame.midX, y: weekdayFrame.midY))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseDayY = maxWeekdayY + weekdayDaySpacing
|
||||||
|
var maxDayY = baseDayY
|
||||||
|
|
||||||
|
for i in 0 ..< updatedDays.count {
|
||||||
|
let gridIndex = (context.component.model.firstDayWeekday - 1) + i
|
||||||
|
let gridX = sideInset + CGFloat(gridIndex % 7) * weekdayWidth
|
||||||
|
let gridY = baseDayY + CGFloat(gridIndex / 7) * (weekdaySize + weekdaySpacing)
|
||||||
|
let dayItemSize = updatedDays[i].size
|
||||||
|
let dayFrame = CGRect(origin: CGPoint(x: gridX + floor((weekdayWidth - dayItemSize.width) / 2.0), y: gridY + floor((weekdaySize - dayItemSize.height) / 2.0)), size: dayItemSize)
|
||||||
|
maxDayY = max(maxDayY, gridY + weekdaySize)
|
||||||
|
context.add(updatedDays[i]
|
||||||
|
.position(CGPoint(x: dayFrame.midX, y: dayFrame.midY))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: context.availableSize.width, height: maxDayY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DayMedia: Equatable {
|
||||||
|
var message: EngineMessage
|
||||||
|
var media: EngineMedia
|
||||||
|
|
||||||
|
static func ==(lhs: DayMedia, rhs: DayMedia) -> Bool {
|
||||||
|
if lhs.message.id != rhs.message.id {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MonthModel: Equatable {
|
||||||
|
var year: Int
|
||||||
|
var index: Int
|
||||||
|
var numberOfDays: Int
|
||||||
|
var firstDay: Date
|
||||||
|
var firstDayWeekday: Int
|
||||||
|
var currentYear: Int
|
||||||
|
var currentMonth: Int
|
||||||
|
var currentDayOfMonth: Int
|
||||||
|
var mediaByDay: [Int: DayMedia]
|
||||||
|
|
||||||
|
init(
|
||||||
|
year: Int,
|
||||||
|
index: Int,
|
||||||
|
numberOfDays: Int,
|
||||||
|
firstDay: Date,
|
||||||
|
firstDayWeekday: Int,
|
||||||
|
currentYear: Int,
|
||||||
|
currentMonth: Int,
|
||||||
|
currentDayOfMonth: Int,
|
||||||
|
mediaByDay: [Int: DayMedia]
|
||||||
|
) {
|
||||||
|
self.year = year
|
||||||
|
self.index = index
|
||||||
|
self.numberOfDays = numberOfDays
|
||||||
|
self.firstDay = firstDay
|
||||||
|
self.firstDayWeekday = firstDayWeekday
|
||||||
|
self.currentYear = currentYear
|
||||||
|
self.currentMonth = currentMonth
|
||||||
|
self.currentDayOfMonth = currentDayOfMonth
|
||||||
|
self.mediaByDay = mediaByDay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func monthMetadata(calendar: Calendar, for baseDate: Date, currentYear: Int, currentMonth: Int, currentDayOfMonth: Int) -> MonthModel? {
|
||||||
|
guard let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: baseDate)?.count, let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = calendar.component(.year, from: firstDayOfMonth)
|
||||||
|
let month = calendar.component(.month, from: firstDayOfMonth)
|
||||||
|
let firstDayWeekday = calendar.component(.weekday, from: firstDayOfMonth)
|
||||||
|
|
||||||
|
return MonthModel(
|
||||||
|
year: year,
|
||||||
|
index: month,
|
||||||
|
numberOfDays: numberOfDaysInMonth,
|
||||||
|
firstDay: firstDayOfMonth,
|
||||||
|
firstDayWeekday: firstDayWeekday,
|
||||||
|
currentYear: currentYear,
|
||||||
|
currentMonth: currentMonth,
|
||||||
|
currentDayOfMonth: currentDayOfMonth,
|
||||||
|
mediaByDay: [:]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class CalendarMessageScreen: ViewController {
|
||||||
|
private final class Node: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let peerId: PeerId
|
||||||
|
private let navigateToDay: (Int32) -> Void
|
||||||
|
|
||||||
|
private var presentationData: PresentationData
|
||||||
|
private var scrollView: Scroller
|
||||||
|
|
||||||
|
private var initialMonthIndex: Int = 0
|
||||||
|
private var months: [MonthModel] = []
|
||||||
|
private var monthViews: [Int: ComponentHostView<Empty>] = [:]
|
||||||
|
|
||||||
|
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||||
|
private var scrollLayout: (width: CGFloat, contentHeight: CGFloat, frames: [Int: CGRect])?
|
||||||
|
|
||||||
|
init(context: AccountContext, peerId: PeerId, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.peerId = peerId
|
||||||
|
self.navigateToDay = navigateToDay
|
||||||
|
|
||||||
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
self.scrollView = Scroller()
|
||||||
|
self.scrollView.showsVerticalScrollIndicator = true
|
||||||
|
self.scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
self.scrollView.scrollsToTop = false
|
||||||
|
self.scrollView.delaysContentTouches = false
|
||||||
|
self.scrollView.canCancelContentTouches = true
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
self.scrollView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||||
|
self.scrollView.disablesInteractiveModalDismiss = true
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let calendar = Calendar(identifier: .gregorian)
|
||||||
|
|
||||||
|
let baseDate = Date()
|
||||||
|
let currentYear = calendar.component(.year, from: baseDate)
|
||||||
|
let currentMonth = calendar.component(.month, from: baseDate)
|
||||||
|
let currentDayOfMonth = calendar.component(.day, from: baseDate)
|
||||||
|
|
||||||
|
let initialDate = Date(timeIntervalSince1970: TimeInterval(initialTimestamp))
|
||||||
|
|
||||||
|
for i in 0 ..< 12 * 20 {
|
||||||
|
guard let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
guard let monthBaseDate = calendar.date(byAdding: .month, value: -i, to: firstDayOfMonth) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
guard let monthModel = monthMetadata(calendar: calendar, for: monthBaseDate, currentYear: currentYear, currentMonth: currentMonth, currentDayOfMonth: currentDayOfMonth) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if monthModel.year < 2013 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if monthModel.year == 2013 {
|
||||||
|
if monthModel.index < 8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.months.append(monthModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.months.count > 1 {
|
||||||
|
for i in 0 ..< self.months.count - 1 {
|
||||||
|
if initialDate >= self.months[i].firstDay {
|
||||||
|
self.initialMonthIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||||
|
|
||||||
|
self.scrollView.delegate = self
|
||||||
|
self.view.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.reloadMediaInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
let isFirstLayout = self.validLayout == nil
|
||||||
|
self.validLayout = (layout, navigationHeight)
|
||||||
|
|
||||||
|
if self.updateScrollLayoutIfNeeded() {
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFirstLayout, let frame = self.scrollLayout?.frames[self.initialMonthIndex] {
|
||||||
|
var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0)
|
||||||
|
if contentOffset < 0 {
|
||||||
|
contentOffset = 0
|
||||||
|
}
|
||||||
|
if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height {
|
||||||
|
contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height
|
||||||
|
}
|
||||||
|
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMonthViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
self.updateMonthViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateScrollLayoutIfNeeded() -> Bool {
|
||||||
|
guard let (layout, navigationHeight) = self.validLayout else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.scrollLayout?.width == layout.size.width {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentHeight: CGFloat = layout.intrinsicInsets.bottom
|
||||||
|
var frames: [Int: CGRect] = [:]
|
||||||
|
|
||||||
|
let measureView = ComponentHostView<Empty>()
|
||||||
|
for i in 0 ..< self.months.count {
|
||||||
|
let monthSize = measureView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MonthComponent(
|
||||||
|
context: self.context,
|
||||||
|
model: self.months[i],
|
||||||
|
foregroundColor: .black,
|
||||||
|
strings: self.presentationData.strings,
|
||||||
|
theme: self.presentationData.theme,
|
||||||
|
navigateToDay: { _ in
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: layout.size.width, height: 10000.0
|
||||||
|
))
|
||||||
|
let monthFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: monthSize)
|
||||||
|
contentHeight += monthSize.height
|
||||||
|
if i != self.months.count {
|
||||||
|
contentHeight += 16.0
|
||||||
|
}
|
||||||
|
frames[i] = monthFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollLayout = (layout.size.width, contentHeight, frames)
|
||||||
|
|
||||||
|
self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight))
|
||||||
|
self.scrollView.contentSize = CGSize(width: layout.size.width, height: contentHeight)
|
||||||
|
self.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: layout.intrinsicInsets.bottom, left: 0.0, bottom: 0.0, right: 0.0)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMonthViews() {
|
||||||
|
guard let (width, _, frames) = self.scrollLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleRect = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||||
|
var validMonths = Set<Int>()
|
||||||
|
|
||||||
|
for i in 0 ..< self.months.count {
|
||||||
|
guard let monthFrame = frames[i] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !visibleRect.intersects(monthFrame) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validMonths.insert(i)
|
||||||
|
|
||||||
|
let monthView: ComponentHostView<Empty>
|
||||||
|
if let current = self.monthViews[i] {
|
||||||
|
monthView = current
|
||||||
|
} else {
|
||||||
|
monthView = ComponentHostView()
|
||||||
|
monthView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||||
|
self.monthViews[i] = monthView
|
||||||
|
self.scrollView.addSubview(monthView)
|
||||||
|
}
|
||||||
|
let _ = monthView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MonthComponent(
|
||||||
|
context: self.context,
|
||||||
|
model: self.months[i],
|
||||||
|
foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor,
|
||||||
|
strings: self.presentationData.strings,
|
||||||
|
theme: self.presentationData.theme,
|
||||||
|
navigateToDay: { [weak self] timestamp in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.navigateToDay(timestamp)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: width, height: 10000.0
|
||||||
|
))
|
||||||
|
monthView.frame = monthFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeMonths: [Int] = []
|
||||||
|
for (index, view) in self.monthViews {
|
||||||
|
if !validMonths.contains(index) {
|
||||||
|
view.removeFromSuperview()
|
||||||
|
removeMonths.append(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index in removeMonths {
|
||||||
|
self.monthViews.removeValue(forKey: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadMediaInfo() {
|
||||||
|
let peerId = self.peerId
|
||||||
|
let months = self.months
|
||||||
|
let _ = (self.context.account.postbox.transaction { transaction -> [Int: [Int: DayMedia]] in
|
||||||
|
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
||||||
|
|
||||||
|
for i in 0 ..< months.count {
|
||||||
|
for day in 0 ..< months[i].numberOfDays {
|
||||||
|
let dayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day)
|
||||||
|
let nextDayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day - 1)
|
||||||
|
if let message = transaction.firstMessageInRange(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: .photoOrVideo, timestampMax: dayTimestamp, timestampMin: nextDayTimestamp - 1) {
|
||||||
|
/*if message.timestamp < nextDayTimestamp {
|
||||||
|
continue
|
||||||
|
}*/
|
||||||
|
if updatedMedia[i] == nil {
|
||||||
|
updatedMedia[i] = [:]
|
||||||
|
}
|
||||||
|
mediaLoop: for media in message.media {
|
||||||
|
switch media {
|
||||||
|
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
||||||
|
updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media))
|
||||||
|
break mediaLoop
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedMedia
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] updatedMedia in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (monthIndex, mediaByDay) in updatedMedia {
|
||||||
|
strongSelf.months[monthIndex].mediaByDay = mediaByDay
|
||||||
|
}
|
||||||
|
strongSelf.updateMonthViews()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var node: Node {
|
||||||
|
return self.displayNode as! Node
|
||||||
|
}
|
||||||
|
|
||||||
|
private let context: AccountContext
|
||||||
|
private let peerId: PeerId
|
||||||
|
private let initialTimestamp: Int32
|
||||||
|
private let navigateToDay: (CalendarMessageScreen, Int32) -> Void
|
||||||
|
|
||||||
|
public init(context: AccountContext, peerId: PeerId, initialTimestamp: Int32, navigateToDay: @escaping (CalendarMessageScreen, Int32) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.peerId = peerId
|
||||||
|
self.initialTimestamp = initialTimestamp
|
||||||
|
self.navigateToDay = navigateToDay
|
||||||
|
|
||||||
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
|
||||||
|
|
||||||
|
self.navigationPresentation = .modal
|
||||||
|
|
||||||
|
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
|
||||||
|
//TODO:localize
|
||||||
|
self.navigationItem.setTitle("Jump to Date", animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func dismissPressed() {
|
||||||
|
self.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func loadDisplayNode() {
|
||||||
|
self.displayNode = Node(context: self.context, peerId: self.peerId, initialTimestamp: self.initialTimestamp, navigateToDay: { [weak self] timestamp in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.navigateToDay(strongSelf, timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.displayNodeDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
|
self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -2694,6 +2694,13 @@ final class MessageHistoryTable: Table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func firstMessageInRange(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, timestampMax: Int32, timestampMin: Int32) -> IntermediateMessage? {
|
||||||
|
guard let index = self.tagsTable.earlierIndices(tag: tag, peerId: peerId, namespace: namespace, index: MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: 1), timestamp: timestampMax), includeFrom: true, minIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: 1), timestamp: timestampMin), count: 1).first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return self.getMessage(index)
|
||||||
|
}
|
||||||
|
|
||||||
func incomingMessageStatsInIndices(_ peerId: PeerId, namespace: MessageId.Namespace, indices: [MessageIndex]) -> (Int, Bool) {
|
func incomingMessageStatsInIndices(_ peerId: PeerId, namespace: MessageId.Namespace, indices: [MessageIndex]) -> (Int, Bool) {
|
||||||
var count: Int = 0
|
var count: Int = 0
|
||||||
var holes = false
|
var holes = false
|
||||||
|
@ -84,7 +84,7 @@ class MessageHistoryTagsTable: Table {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func earlierIndices(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace, index: MessageIndex?, includeFrom: Bool, count: Int) -> [MessageIndex] {
|
func earlierIndices(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace, index: MessageIndex?, includeFrom: Bool, minIndex: MessageIndex? = nil, count: Int) -> [MessageIndex] {
|
||||||
var indices: [MessageIndex] = []
|
var indices: [MessageIndex] = []
|
||||||
let key: ValueBoxKey
|
let key: ValueBoxKey
|
||||||
if let index = index {
|
if let index = index {
|
||||||
@ -96,7 +96,13 @@ class MessageHistoryTagsTable: Table {
|
|||||||
} else {
|
} else {
|
||||||
key = self.upperBound(tag: tag, peerId: peerId, namespace: namespace)
|
key = self.upperBound(tag: tag, peerId: peerId, namespace: namespace)
|
||||||
}
|
}
|
||||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in
|
let endKey: ValueBoxKey
|
||||||
|
if let minIndex = minIndex {
|
||||||
|
endKey = self.key(tag: tag, index: minIndex)
|
||||||
|
} else {
|
||||||
|
endKey = self.lowerBound(tag: tag, peerId: peerId, namespace: namespace)
|
||||||
|
}
|
||||||
|
self.valueBox.range(self.table, start: key, end: endKey, keys: { key in
|
||||||
indices.append(extractKey(key))
|
indices.append(extractKey(key))
|
||||||
return true
|
return true
|
||||||
}, limit: count)
|
}, limit: count)
|
||||||
|
@ -709,6 +709,15 @@ public final class Transaction {
|
|||||||
return self.postbox?.messageHistoryTable.findRandomMessage(peerId: peerId, namespace: namespace, tag: tag, ignoreIds: ignoreIds)
|
return self.postbox?.messageHistoryTable.findRandomMessage(peerId: peerId, namespace: namespace, tag: tag, ignoreIds: ignoreIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func firstMessageInRange(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, timestampMax: Int32, timestampMin: Int32) -> Message? {
|
||||||
|
assert(!self.disposed)
|
||||||
|
if let message = self.postbox?.messageHistoryTable.firstMessageInRange(peerId: peerId, namespace: namespace, tag: tag, timestampMax: timestampMax, timestampMin: timestampMin) {
|
||||||
|
return self.postbox?.renderIntermediateMessage(message)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func filterStoredMessageIds(_ messageIds: Set<MessageId>) -> Set<MessageId> {
|
public func filterStoredMessageIds(_ messageIds: Set<MessageId>) -> Set<MessageId> {
|
||||||
assert(!self.disposed)
|
assert(!self.disposed)
|
||||||
if let postbox = self.postbox {
|
if let postbox = self.postbox {
|
||||||
|
@ -319,7 +319,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 indicatorPosition: CGFloat?
|
||||||
|
private var scrollIndicatorHeight: CGFloat?
|
||||||
|
|
||||||
private var dragGesture: DragGesture?
|
private var dragGesture: DragGesture?
|
||||||
public private(set) var isDragging: Bool = false
|
public private(set) var isDragging: Bool = false
|
||||||
|
|
||||||
@ -370,6 +374,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
|
|
||||||
strongSelf.isDragging = true
|
strongSelf.isDragging = true
|
||||||
|
|
||||||
|
strongSelf.updateLineIndicator(transition: transition)
|
||||||
|
|
||||||
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
|
||||||
@ -388,6 +394,9 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
transition.updateSublayerTransformOffset(layer: strongSelf.dateIndicator.layer, offset: CGPoint(x: 0.0, y: 0.0))
|
transition.updateSublayerTransformOffset(layer: strongSelf.dateIndicator.layer, offset: CGPoint(x: 0.0, y: 0.0))
|
||||||
|
|
||||||
strongSelf.isDragging = false
|
strongSelf.isDragging = false
|
||||||
|
|
||||||
|
strongSelf.updateLineIndicator(transition: transition)
|
||||||
|
|
||||||
strongSelf.updateActivityTimer()
|
strongSelf.updateActivityTimer()
|
||||||
},
|
},
|
||||||
moved: { [weak self] relativeOffset in
|
moved: { [weak self] relativeOffset in
|
||||||
@ -470,7 +479,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
let dateIndicatorTopPosition = topIndicatorInset
|
let dateIndicatorTopPosition = topIndicatorInset
|
||||||
let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - indicatorSize.height
|
let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - indicatorSize.height
|
||||||
|
|
||||||
let indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction
|
self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction
|
||||||
|
self.scrollIndicatorHeight = scrollIndicatorHeight
|
||||||
|
|
||||||
let dateIndicatorPosition = dateIndicatorTopPosition * (1.0 - indicatorPositionFraction) + dateIndicatorBottomPosition * indicatorPositionFraction
|
let dateIndicatorPosition = dateIndicatorTopPosition * (1.0 - indicatorPositionFraction) + dateIndicatorBottomPosition * indicatorPositionFraction
|
||||||
|
|
||||||
@ -487,7 +497,15 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
self.lineIndicator.alpha = 1.0
|
self.lineIndicator.alpha = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let lineIndicatorSize = CGSize(width: 3.0, height: scrollIndicatorHeight)
|
self.updateLineIndicator(transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
|
||||||
|
guard let indicatorPosition = self.indicatorPosition, let scrollIndicatorHeight = self.scrollIndicatorHeight else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let lineIndicatorSize = CGSize(width: self.isDragging ? 6.0 : 3.0, height: scrollIndicatorHeight)
|
||||||
let _ = self.lineIndicator.update(
|
let _ = self.lineIndicator.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(RoundedRectangle(
|
component: AnyComponent(RoundedRectangle(
|
||||||
@ -497,7 +515,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
containerSize: lineIndicatorSize
|
containerSize: lineIndicatorSize
|
||||||
)
|
)
|
||||||
|
|
||||||
transition.updateFrame(view: self.lineIndicator, frame: CGRect(origin: CGPoint(x: containerSize.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() {
|
||||||
@ -508,7 +526,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0)
|
transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0)
|
||||||
transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0)
|
transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0)
|
||||||
} else {
|
} else {
|
||||||
self.activityTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
self.activityTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -522,7 +540,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
|
|
||||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.dateIndicator.frame.contains(point) {
|
if self.dateIndicator.frame.contains(point) {
|
||||||
return self.view
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -291,7 +291,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) }
|
dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) }
|
||||||
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
||||||
dict[-1072953408] = { return Api.ChannelParticipant.parse_channelParticipant($0) }
|
dict[-1072953408] = { return Api.ChannelParticipant.parse_channelParticipant($0) }
|
||||||
dict[682146919] = { return Api.ChannelParticipant.parse_channelParticipantSelf($0) }
|
dict[900251559] = { return Api.ChannelParticipant.parse_channelParticipantSelf($0) }
|
||||||
dict[803602899] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) }
|
dict[803602899] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) }
|
||||||
dict[885242707] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) }
|
dict[885242707] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) }
|
||||||
dict[1844969806] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) }
|
dict[1844969806] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) }
|
||||||
@ -472,6 +472,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-1271602504] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) }
|
dict[-1271602504] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) }
|
||||||
dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) }
|
dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) }
|
||||||
dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) }
|
dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) }
|
||||||
|
dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) }
|
||||||
dict[-1269012015] = { return Api.messages.AffectedHistory.parse_affectedHistory($0) }
|
dict[-1269012015] = { return Api.messages.AffectedHistory.parse_affectedHistory($0) }
|
||||||
dict[1244130093] = { return Api.StatsGraph.parse_statsGraphAsync($0) }
|
dict[1244130093] = { return Api.StatsGraph.parse_statsGraphAsync($0) }
|
||||||
dict[-1092839390] = { return Api.StatsGraph.parse_statsGraphError($0) }
|
dict[-1092839390] = { return Api.StatsGraph.parse_statsGraphError($0) }
|
||||||
@ -562,6 +563,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-1078612597] = { return Api.ChannelLocation.parse_channelLocationEmpty($0) }
|
dict[-1078612597] = { return Api.ChannelLocation.parse_channelLocationEmpty($0) }
|
||||||
dict[547062491] = { return Api.ChannelLocation.parse_channelLocation($0) }
|
dict[547062491] = { return Api.ChannelLocation.parse_channelLocation($0) }
|
||||||
dict[182649427] = { return Api.MessageRange.parse_messageRange($0) }
|
dict[182649427] = { return Api.MessageRange.parse_messageRange($0) }
|
||||||
|
dict[1404185519] = { return Api.messages.SearchResultsPositions.parse_searchResultsPositions($0) }
|
||||||
dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) }
|
dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) }
|
||||||
dict[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) }
|
dict[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) }
|
||||||
dict[-478701471] = { return Api.account.ResetPasswordResult.parse_resetPasswordFailedWait($0) }
|
dict[-478701471] = { return Api.account.ResetPasswordResult.parse_resetPasswordFailedWait($0) }
|
||||||
@ -1235,6 +1237,8 @@ public struct Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.SecurePlainData:
|
case let _1 as Api.SecurePlainData:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.SearchResultsPosition:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.messages.AffectedHistory:
|
case let _1 as Api.messages.AffectedHistory:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.StatsGraph:
|
case let _1 as Api.StatsGraph:
|
||||||
@ -1335,6 +1339,8 @@ public struct Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MessageRange:
|
case let _1 as Api.MessageRange:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.messages.SearchResultsPositions:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.messages.StickerSetInstallResult:
|
case let _1 as Api.messages.StickerSetInstallResult:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.account.ResetPasswordResult:
|
case let _1 as Api.account.ResetPasswordResult:
|
||||||
|
@ -1243,6 +1243,50 @@ public struct messages {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public enum SearchResultsPositions: TypeConstructorDescription {
|
||||||
|
case searchResultsPositions(count: Int32, positions: [Api.SearchResultsPosition])
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .searchResultsPositions(let count, let positions):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(1404185519)
|
||||||
|
}
|
||||||
|
serializeInt32(count, buffer: buffer, boxed: false)
|
||||||
|
buffer.appendInt32(481674261)
|
||||||
|
buffer.appendInt32(Int32(positions.count))
|
||||||
|
for item in positions {
|
||||||
|
item.serialize(buffer, true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .searchResultsPositions(let count, let positions):
|
||||||
|
return ("searchResultsPositions", [("count", count), ("positions", positions)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_searchResultsPositions(_ reader: BufferReader) -> SearchResultsPositions? {
|
||||||
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: [Api.SearchResultsPosition]?
|
||||||
|
if let _ = reader.readInt32() {
|
||||||
|
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SearchResultsPosition.self)
|
||||||
|
}
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.messages.SearchResultsPositions.searchResultsPositions(count: _1!, positions: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public enum StickerSetInstallResult: TypeConstructorDescription {
|
public enum StickerSetInstallResult: TypeConstructorDescription {
|
||||||
case stickerSetInstallResultSuccess
|
case stickerSetInstallResultSuccess
|
||||||
|
@ -7617,7 +7617,7 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
public enum ChannelParticipant: TypeConstructorDescription {
|
public enum ChannelParticipant: TypeConstructorDescription {
|
||||||
case channelParticipant(userId: Int64, date: Int32)
|
case channelParticipant(userId: Int64, date: Int32)
|
||||||
case channelParticipantSelf(userId: Int64, inviterId: Int64, date: Int32)
|
case channelParticipantSelf(flags: Int32, userId: Int64, inviterId: Int64, date: Int32)
|
||||||
case channelParticipantCreator(flags: Int32, userId: Int64, adminRights: Api.ChatAdminRights, rank: String?)
|
case channelParticipantCreator(flags: Int32, userId: Int64, adminRights: Api.ChatAdminRights, rank: String?)
|
||||||
case channelParticipantAdmin(flags: Int32, userId: Int64, inviterId: Int64?, promotedBy: Int64, date: Int32, adminRights: Api.ChatAdminRights, rank: String?)
|
case channelParticipantAdmin(flags: Int32, userId: Int64, inviterId: Int64?, promotedBy: Int64, date: Int32, adminRights: Api.ChatAdminRights, rank: String?)
|
||||||
case channelParticipantBanned(flags: Int32, peer: Api.Peer, kickedBy: Int64, date: Int32, bannedRights: Api.ChatBannedRights)
|
case channelParticipantBanned(flags: Int32, peer: Api.Peer, kickedBy: Int64, date: Int32, bannedRights: Api.ChatBannedRights)
|
||||||
@ -7632,10 +7632,11 @@ public extension Api {
|
|||||||
serializeInt64(userId, buffer: buffer, boxed: false)
|
serializeInt64(userId, buffer: buffer, boxed: false)
|
||||||
serializeInt32(date, buffer: buffer, boxed: false)
|
serializeInt32(date, buffer: buffer, boxed: false)
|
||||||
break
|
break
|
||||||
case .channelParticipantSelf(let userId, let inviterId, let date):
|
case .channelParticipantSelf(let flags, let userId, let inviterId, let date):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(682146919)
|
buffer.appendInt32(900251559)
|
||||||
}
|
}
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeInt64(userId, buffer: buffer, boxed: false)
|
serializeInt64(userId, buffer: buffer, boxed: false)
|
||||||
serializeInt64(inviterId, buffer: buffer, boxed: false)
|
serializeInt64(inviterId, buffer: buffer, boxed: false)
|
||||||
serializeInt32(date, buffer: buffer, boxed: false)
|
serializeInt32(date, buffer: buffer, boxed: false)
|
||||||
@ -7684,8 +7685,8 @@ public extension Api {
|
|||||||
switch self {
|
switch self {
|
||||||
case .channelParticipant(let userId, let date):
|
case .channelParticipant(let userId, let date):
|
||||||
return ("channelParticipant", [("userId", userId), ("date", date)])
|
return ("channelParticipant", [("userId", userId), ("date", date)])
|
||||||
case .channelParticipantSelf(let userId, let inviterId, let date):
|
case .channelParticipantSelf(let flags, let userId, let inviterId, let date):
|
||||||
return ("channelParticipantSelf", [("userId", userId), ("inviterId", inviterId), ("date", date)])
|
return ("channelParticipantSelf", [("flags", flags), ("userId", userId), ("inviterId", inviterId), ("date", date)])
|
||||||
case .channelParticipantCreator(let flags, let userId, let adminRights, let rank):
|
case .channelParticipantCreator(let flags, let userId, let adminRights, let rank):
|
||||||
return ("channelParticipantCreator", [("flags", flags), ("userId", userId), ("adminRights", adminRights), ("rank", rank)])
|
return ("channelParticipantCreator", [("flags", flags), ("userId", userId), ("adminRights", adminRights), ("rank", rank)])
|
||||||
case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank):
|
case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank):
|
||||||
@ -7712,17 +7713,20 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func parse_channelParticipantSelf(_ reader: BufferReader) -> ChannelParticipant? {
|
public static func parse_channelParticipantSelf(_ reader: BufferReader) -> ChannelParticipant? {
|
||||||
var _1: Int64?
|
var _1: Int32?
|
||||||
_1 = reader.readInt64()
|
_1 = reader.readInt32()
|
||||||
var _2: Int64?
|
var _2: Int64?
|
||||||
_2 = reader.readInt64()
|
_2 = reader.readInt64()
|
||||||
var _3: Int32?
|
var _3: Int64?
|
||||||
_3 = reader.readInt32()
|
_3 = reader.readInt64()
|
||||||
|
var _4: Int32?
|
||||||
|
_4 = reader.readInt32()
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
if _c1 && _c2 && _c3 {
|
let _c4 = _4 != nil
|
||||||
return Api.ChannelParticipant.channelParticipantSelf(userId: _1!, inviterId: _2!, date: _3!)
|
if _c1 && _c2 && _c3 && _c4 {
|
||||||
|
return Api.ChannelParticipant.channelParticipantSelf(flags: _1!, userId: _2!, inviterId: _3!, date: _4!)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
@ -12202,6 +12206,48 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public enum SearchResultsPosition: TypeConstructorDescription {
|
||||||
|
case searchResultPosition(msgId: Int32, date: Int32, offset: Int32)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .searchResultPosition(let msgId, let date, let offset):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(2137295719)
|
||||||
|
}
|
||||||
|
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(date, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(offset, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .searchResultPosition(let msgId, let date, let offset):
|
||||||
|
return ("searchResultPosition", [("msgId", msgId), ("date", date), ("offset", offset)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_searchResultPosition(_ reader: BufferReader) -> SearchResultsPosition? {
|
||||||
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: Int32?
|
||||||
|
_2 = reader.readInt32()
|
||||||
|
var _3: Int32?
|
||||||
|
_3 = reader.readInt32()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
let _c3 = _3 != nil
|
||||||
|
if _c1 && _c2 && _c3 {
|
||||||
|
return Api.SearchResultsPosition.searchResultPosition(msgId: _1!, date: _2!, offset: _3!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public enum StatsGraph: TypeConstructorDescription {
|
public enum StatsGraph: TypeConstructorDescription {
|
||||||
case statsGraphAsync(token: String)
|
case statsGraphAsync(token: String)
|
||||||
|
@ -4484,6 +4484,23 @@ public extension Api {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getSearchResultsPositions(peer: Api.InputPeer, filter: Api.MessagesFilter, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.SearchResultsPositions>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(1855292323)
|
||||||
|
peer.serialize(buffer, true)
|
||||||
|
filter.serialize(buffer, true)
|
||||||
|
serializeInt32(offsetId, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||||
|
return (FunctionDescription(name: "messages.getSearchResultsPositions", parameters: [("peer", peer), ("filter", filter), ("offsetId", offsetId), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SearchResultsPositions? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: Api.messages.SearchResultsPositions?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
result = Api.parse(reader, signature: signature) as? Api.messages.SearchResultsPositions
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public static func hideChatJoinRequest(flags: Int32, peer: Api.InputPeer, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
public static func hideChatJoinRequest(flags: Int32, peer: Api.InputPeer, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(2145904661)
|
buffer.appendInt32(2145904661)
|
||||||
|
@ -207,7 +207,7 @@ extension ChannelParticipant {
|
|||||||
self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil)
|
self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil)
|
||||||
case let .channelParticipantAdmin(flags, userId, _, promotedBy, date, adminRights, rank: rank):
|
case let .channelParticipantAdmin(flags, userId, _, promotedBy, date, adminRights, rank: rank):
|
||||||
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank)
|
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank)
|
||||||
case let .channelParticipantSelf(userId, _, date):
|
case let .channelParticipantSelf(_, userId, _, date):
|
||||||
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil)
|
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil)
|
||||||
case let .channelParticipantLeft(userId):
|
case let .channelParticipantLeft(userId):
|
||||||
self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil)
|
self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil)
|
||||||
|
@ -158,6 +158,7 @@ struct FetchMessageHistoryHoleResult: Equatable {
|
|||||||
var strictRemovedIndices: IndexSet
|
var strictRemovedIndices: IndexSet
|
||||||
var actualPeerId: PeerId?
|
var actualPeerId: PeerId?
|
||||||
var actualThreadId: Int64?
|
var actualThreadId: Int64?
|
||||||
|
var ids: [MessageId]
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryHoleSource, postbox: Postbox, peerInput: FetchMessageHistoryHoleThreadInput, namespace: MessageId.Namespace, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, count rawCount: Int) -> Signal<FetchMessageHistoryHoleResult?, NoError> {
|
func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryHoleSource, postbox: Postbox, peerInput: FetchMessageHistoryHoleThreadInput, namespace: MessageId.Namespace, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, count rawCount: Int) -> Signal<FetchMessageHistoryHoleResult?, NoError> {
|
||||||
@ -183,7 +184,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH
|
|||||||
}
|
}
|
||||||
|> mapToSignal { (inputPeer, hash) -> Signal<FetchMessageHistoryHoleResult?, NoError> in
|
|> mapToSignal { (inputPeer, hash) -> Signal<FetchMessageHistoryHoleResult?, NoError> in
|
||||||
guard let inputPeer = inputPeer else {
|
guard let inputPeer = inputPeer else {
|
||||||
return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil))
|
return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []))
|
||||||
}
|
}
|
||||||
|
|
||||||
print("fetchMessageHistoryHole for \(peerInput) direction \(direction) space \(space)")
|
print("fetchMessageHistoryHole for \(peerInput) direction \(direction) space \(space)")
|
||||||
@ -501,6 +502,23 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let fullIds = storeMessages.compactMap { message -> MessageId? in
|
||||||
|
switch message.id {
|
||||||
|
case let .Id(id):
|
||||||
|
switch space {
|
||||||
|
case let .tag(tag):
|
||||||
|
if !message.tags.contains(tag) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
case .everywhere:
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
case .Partial:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if ids.count == 0 || implicitelyFillHole {
|
if ids.count == 0 || implicitelyFillHole {
|
||||||
filledRange = minMaxRange
|
filledRange = minMaxRange
|
||||||
strictFilledIndices = IndexSet()
|
strictFilledIndices = IndexSet()
|
||||||
@ -561,7 +579,8 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH
|
|||||||
removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)),
|
removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)),
|
||||||
strictRemovedIndices: strictFilledIndices,
|
strictRemovedIndices: strictFilledIndices,
|
||||||
actualPeerId: storeMessages.first?.id.peerId,
|
actualPeerId: storeMessages.first?.id.peerId,
|
||||||
actualThreadId: storeMessages.first?.threadId
|
actualThreadId: storeMessages.first?.threadId,
|
||||||
|
ids: fullIds
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -779,7 +779,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa
|
|||||||
let preloadedHistory = preloadedHistoryPosition
|
let preloadedHistory = preloadedHistoryPosition
|
||||||
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
|
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
|
||||||
guard let maxMessageId = maxMessageId else {
|
guard let maxMessageId = maxMessageId else {
|
||||||
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil), .automatic))
|
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), .automatic))
|
||||||
}
|
}
|
||||||
return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
|
return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
|
||||||
if let threadMessageId = threadMessageId {
|
if let threadMessageId = threadMessageId {
|
||||||
@ -832,7 +832,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa
|
|||||||
initialAnchor = .automatic
|
initialAnchor = .automatic
|
||||||
}
|
}
|
||||||
|
|
||||||
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil), initialAnchor))
|
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), initialAnchor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,17 +28,61 @@ public final class SparseMessageList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ItemIndices: Equatable {
|
private struct SparseItems: Equatable {
|
||||||
var ids: [MessageId]
|
enum Item: Equatable {
|
||||||
var timestamps: [Int32]
|
case range(count: Int)
|
||||||
|
case anchor(id: MessageId, timestamp: Int32, message: Message?)
|
||||||
|
|
||||||
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .range(count):
|
||||||
|
if case .range(count) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .anchor(lhsId, lhsTimestamp, lhsMessage):
|
||||||
|
if case let .anchor(rhsId, rhsTimestamp, rhsMessage) = rhs {
|
||||||
|
if lhsId != rhsId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhsTimestamp != rhsTimestamp {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if let lhsMessage = lhsMessage, let rhsMessage = rhsMessage {
|
||||||
|
if lhsMessage.id != rhsMessage.id {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhsMessage.stableVersion != rhsMessage.stableVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (lhsMessage != nil) != (rhsMessage != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [Item]
|
||||||
}
|
}
|
||||||
|
|
||||||
private var topSectionItemRequestCount: Int = 100
|
private var topSectionItemRequestCount: Int = 100
|
||||||
private var topSection: TopSection?
|
private var topSection: TopSection?
|
||||||
private var topItemsDisposable: Disposable?
|
private var topItemsDisposable: Disposable?
|
||||||
|
|
||||||
private var messageIndices: ItemIndices?
|
private var sparseItems: SparseItems?
|
||||||
private var messageIndicesDisposable: Disposable?
|
private var sparseItemsDisposable: Disposable?
|
||||||
|
|
||||||
|
private struct LoadingHole: Equatable {
|
||||||
|
var anchor: MessageId
|
||||||
|
var direction: LoadHoleDirection
|
||||||
|
}
|
||||||
|
private let loadHoleDisposable = MetaDisposable()
|
||||||
|
private var loadingHole: LoadingHole?
|
||||||
|
|
||||||
private var loadingPlaceholders: [MessageId: Disposable] = [:]
|
private var loadingPlaceholders: [MessageId: Disposable] = [:]
|
||||||
private var loadedPlaceholders: [MessageId: Message] = [:]
|
private var loadedPlaceholders: [MessageId: Message] = [:]
|
||||||
@ -52,31 +96,67 @@ public final class SparseMessageList {
|
|||||||
self.messageTag = messageTag
|
self.messageTag = messageTag
|
||||||
|
|
||||||
self.resetTopSection()
|
self.resetTopSection()
|
||||||
self.messageIndicesDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
|
||||||
|
self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||||
}
|
}
|
||||||
|> mapToSignal { inputPeer -> Signal<ItemIndices, NoError> in
|
|> mapToSignal { inputPeer -> Signal<SparseItems, NoError> in
|
||||||
guard let inputPeer = inputPeer else {
|
guard let inputPeer = inputPeer else {
|
||||||
return .single(ItemIndices(ids: [], timestamps: []))
|
return .single(SparseItems(items: []))
|
||||||
}
|
}
|
||||||
return self.account.network.request(Api.functions.messages.getSearchResultsRawMessages(peer: inputPeer, filter: .inputMessagesFilterPhotoVideo, offsetId: 0, offsetDate: 0))
|
return account.network.request(Api.functions.messages.getSearchResultsPositions(peer: inputPeer, filter: .inputMessagesFilterPhotoVideo, offsetId: 0, limit: 1000))
|
||||||
|> map { result -> ItemIndices in
|
|> map { result -> SparseItems in
|
||||||
switch result {
|
switch result {
|
||||||
case let .searchResultsRawMessages(msgIds, msgDates):
|
case let .searchResultsPositions(totalCount, positions):
|
||||||
return ItemIndices(ids: msgIds.map { id in
|
struct Position {
|
||||||
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)
|
var id: Int32
|
||||||
}, timestamps: msgDates)
|
var date: Int32
|
||||||
|
var offset: Int
|
||||||
|
}
|
||||||
|
let positions: [Position] = positions.sorted(by: { lhs, rhs in
|
||||||
|
switch lhs {
|
||||||
|
case let .searchResultPosition(lhsId, _, _):
|
||||||
|
switch rhs {
|
||||||
|
case let .searchResultPosition(rhsId, _, _):
|
||||||
|
return lhsId > rhsId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> `catch` { _ -> Signal<ItemIndices, NoError> in
|
}).map { position -> Position in
|
||||||
return .single(ItemIndices(ids: [], timestamps: []))
|
switch position {
|
||||||
|
case let .searchResultPosition(id, date, offset):
|
||||||
|
return Position(id: id, date: date, offset: Int(offset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] indices in
|
|
||||||
|
var result = SparseItems(items: [])
|
||||||
|
for i in 0 ..< positions.count {
|
||||||
|
if i != 0 {
|
||||||
|
let deltaCount = positions[i].offset - 1 - positions[i - 1].offset
|
||||||
|
if deltaCount > 0 {
|
||||||
|
result.items.append(.range(count: deltaCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.items.append(.anchor(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: positions[i].id), timestamp: positions[i].date, message: nil))
|
||||||
|
if i == positions.count - 1 {
|
||||||
|
let remainingCount = Int(totalCount) - 1 - positions[i].offset
|
||||||
|
if remainingCount > 0 {
|
||||||
|
result.items.append(.range(count: remainingCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> `catch` { _ -> Signal<SparseItems, NoError> in
|
||||||
|
return .single(SparseItems(items: []))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] sparseItems in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.messageIndices = indices
|
strongSelf.sparseItems = sparseItems
|
||||||
if strongSelf.topSection != nil {
|
if strongSelf.topSection != nil {
|
||||||
strongSelf.updateState()
|
strongSelf.updateState()
|
||||||
}
|
}
|
||||||
@ -85,11 +165,12 @@ public final class SparseMessageList {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.topItemsDisposable?.dispose()
|
self.topItemsDisposable?.dispose()
|
||||||
self.messageIndicesDisposable?.dispose()
|
self.sparseItemsDisposable?.dispose()
|
||||||
|
self.loadHoleDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetTopSection() {
|
private func resetTopSection() {
|
||||||
self.topItemsDisposable = (self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .upperBound, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: self.messageTag, appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: [])
|
self.topItemsDisposable = (self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .upperBound, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: self.messageTag, appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: [])
|
||||||
|> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in
|
|> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -240,18 +321,181 @@ public final class SparseMessageList {
|
|||||||
for message in messages {
|
for message in messages {
|
||||||
strongSelf.loadedPlaceholders[message.id] = message
|
strongSelf.loadedPlaceholders[message.id] = message
|
||||||
}
|
}
|
||||||
|
if strongSelf.sparseItems != nil {
|
||||||
|
for i in 0 ..< strongSelf.sparseItems!.items.count {
|
||||||
|
switch strongSelf.sparseItems!.items[i] {
|
||||||
|
case let .anchor(id, timestamp, _):
|
||||||
|
if let message = strongSelf.loadedPlaceholders[id] {
|
||||||
|
strongSelf.sparseItems!.items[i] = .anchor(id: id, timestamp: timestamp, message: message)
|
||||||
|
}
|
||||||
|
case .range:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.updateState()
|
strongSelf.updateState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadHole(anchor: MessageId, direction: LoadHoleDirection) {
|
||||||
|
let loadingHole = LoadingHole(anchor: anchor, direction: direction)
|
||||||
|
if self.loadingHole == loadingHole {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.loadingHole = loadingHole
|
||||||
|
let mappedDirection: MessageHistoryViewRelativeHoleDirection
|
||||||
|
switch direction {
|
||||||
|
case .around:
|
||||||
|
mappedDirection = .aroundId(anchor)
|
||||||
|
case .earlier:
|
||||||
|
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: 1))
|
||||||
|
case .later:
|
||||||
|
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: Int32.max - 1))
|
||||||
|
}
|
||||||
|
let account = self.account
|
||||||
|
self.loadHoleDisposable.set((fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: self.peerId, threadId: nil), namespace: Namespaces.Message.Cloud, direction: mappedDirection, space: .tag(self.messageTag), count: 100)
|
||||||
|
|> mapToSignal { result -> Signal<[Message], NoError> in
|
||||||
|
guard let result = result else {
|
||||||
|
return .single([])
|
||||||
|
}
|
||||||
|
return account.postbox.transaction { transaction -> [Message] in
|
||||||
|
return result.ids.sorted(by: { $0 > $1 }).compactMap(transaction.getMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] messages in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strongSelf.sparseItems != nil {
|
||||||
|
var sparseHoles: [(itemIndex: Int, leftId: MessageId, rightId: MessageId)] = []
|
||||||
|
for i in 0 ..< strongSelf.sparseItems!.items.count {
|
||||||
|
switch strongSelf.sparseItems!.items[i] {
|
||||||
|
case let .anchor(id, timestamp, _):
|
||||||
|
for messageIndex in 0 ..< messages.count {
|
||||||
|
if messages[messageIndex].id == id {
|
||||||
|
strongSelf.sparseItems!.items[i] = .anchor(id: id, timestamp: timestamp, message: messages[messageIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .range:
|
||||||
|
if i == 0 {
|
||||||
|
assertionFailure()
|
||||||
|
} else {
|
||||||
|
var leftId: MessageId?
|
||||||
|
switch strongSelf.sparseItems!.items[i - 1] {
|
||||||
|
case .range:
|
||||||
|
assertionFailure()
|
||||||
|
case let .anchor(id, _, _):
|
||||||
|
leftId = id
|
||||||
|
}
|
||||||
|
var rightId: MessageId?
|
||||||
|
if i != strongSelf.sparseItems!.items.count - 1 {
|
||||||
|
switch strongSelf.sparseItems!.items[i + 1] {
|
||||||
|
case .range:
|
||||||
|
assertionFailure()
|
||||||
|
case let .anchor(id, _, _):
|
||||||
|
rightId = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let leftId = leftId, let rightId = rightId {
|
||||||
|
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: rightId))
|
||||||
|
} else if let leftId = leftId, i == strongSelf.sparseItems!.items.count - 1 {
|
||||||
|
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: MessageId(peerId: leftId.peerId, namespace: leftId.namespace, id: 1)))
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (itemIndex, initialLeftId, initialRightId) in sparseHoles.reversed() {
|
||||||
|
var leftCovered = false
|
||||||
|
var rightCovered = false
|
||||||
|
for message in messages {
|
||||||
|
if message.id == initialLeftId {
|
||||||
|
leftCovered = true
|
||||||
|
}
|
||||||
|
if message.id == initialRightId {
|
||||||
|
rightCovered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if leftCovered && rightCovered {
|
||||||
|
strongSelf.sparseItems!.items.remove(at: itemIndex)
|
||||||
|
var insertIndex = itemIndex
|
||||||
|
for message in messages {
|
||||||
|
if message.id < initialLeftId && message.id > initialRightId {
|
||||||
|
strongSelf.sparseItems!.items.insert(.anchor(id: message.id, timestamp: message.timestamp, message: message), at: insertIndex)
|
||||||
|
insertIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if leftCovered {
|
||||||
|
for i in 0 ..< messages.count {
|
||||||
|
if messages[i].id == initialLeftId {
|
||||||
|
var spaceItemIndex = itemIndex
|
||||||
|
for j in i + 1 ..< messages.count {
|
||||||
|
switch strongSelf.sparseItems!.items[spaceItemIndex] {
|
||||||
|
case let .range(count):
|
||||||
|
strongSelf.sparseItems!.items[spaceItemIndex] = .range(count: count - 1)
|
||||||
|
case .anchor:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: spaceItemIndex)
|
||||||
|
spaceItemIndex += 1
|
||||||
|
}
|
||||||
|
switch strongSelf.sparseItems!.items[spaceItemIndex] {
|
||||||
|
case let .range(count):
|
||||||
|
if count <= 0 {
|
||||||
|
strongSelf.sparseItems!.items.remove(at: spaceItemIndex)
|
||||||
|
}
|
||||||
|
case .anchor:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if rightCovered {
|
||||||
|
for i in (0 ..< messages.count).reversed() {
|
||||||
|
if messages[i].id == initialRightId {
|
||||||
|
for j in (0 ..< i).reversed() {
|
||||||
|
switch strongSelf.sparseItems!.items[itemIndex] {
|
||||||
|
case let .range(count):
|
||||||
|
strongSelf.sparseItems!.items[itemIndex] = .range(count: count - 1)
|
||||||
|
case .anchor:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: itemIndex + 1)
|
||||||
|
}
|
||||||
|
switch strongSelf.sparseItems!.items[itemIndex] {
|
||||||
|
case let .range(count):
|
||||||
|
if count <= 0 {
|
||||||
|
strongSelf.sparseItems!.items.remove(at: itemIndex)
|
||||||
|
}
|
||||||
|
case .anchor:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
if strongSelf.loadingHole == loadingHole {
|
||||||
|
strongSelf.loadingHole = nil
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
private func updateTopSection(view: MessageHistoryView) {
|
private func updateTopSection(view: MessageHistoryView) {
|
||||||
var topSection: TopSection?
|
var topSection: TopSection?
|
||||||
|
|
||||||
if view.isLoading {
|
if view.isLoading {
|
||||||
topSection = nil
|
topSection = nil
|
||||||
} else {
|
} else {
|
||||||
topSection = TopSection(messages: view.entries.map { entry in
|
topSection = TopSection(messages: view.entries.lazy.reversed().map { entry in
|
||||||
return entry.message
|
return entry.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -268,7 +512,7 @@ public final class SparseMessageList {
|
|||||||
if let topSection = self.topSection {
|
if let topSection = self.topSection {
|
||||||
for i in 0 ..< topSection.messages.count {
|
for i in 0 ..< topSection.messages.count {
|
||||||
let message = topSection.messages[i]
|
let message = topSection.messages[i]
|
||||||
items.append(SparseMessageList.State.Item(index: items.count, content: .message(message)))
|
items.append(SparseMessageList.State.Item(index: items.count, content: .message(message: message, isLocal: true)))
|
||||||
if let minMessageIdValue = minMessageId {
|
if let minMessageIdValue = minMessageId {
|
||||||
if message.id < minMessageIdValue {
|
if message.id < minMessageIdValue {
|
||||||
minMessageId = message.id
|
minMessageId = message.id
|
||||||
@ -279,23 +523,40 @@ public final class SparseMessageList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let topItemCount = items.count
|
||||||
var totalCount = items.count
|
var totalCount = items.count
|
||||||
if let minMessageId = minMessageId, let messageIndices = self.messageIndices {
|
if let minMessageId = minMessageId, let sparseItems = self.sparseItems {
|
||||||
for i in 0 ..< messageIndices.ids.count {
|
var sparseIndex = 0
|
||||||
if messageIndices.ids[i] < minMessageId {
|
let _ = minMessageId
|
||||||
if let message = self.loadedPlaceholders[messageIndices.ids[i]] {
|
for i in 0 ..< sparseItems.items.count {
|
||||||
items.append(SparseMessageList.State.Item(index: items.count, content: .message(message)))
|
switch sparseItems.items[i] {
|
||||||
|
case let .anchor(id, timestamp, message):
|
||||||
|
if sparseIndex >= topItemCount {
|
||||||
|
if let message = message {
|
||||||
|
items.append(SparseMessageList.State.Item(index: totalCount, content: .message(message: message, isLocal: false)))
|
||||||
} else {
|
} else {
|
||||||
items.append(SparseMessageList.State.Item(index: items.count, content: .placeholder(id: messageIndices.ids[i], timestamp: messageIndices.timestamps[i])))
|
items.append(SparseMessageList.State.Item(index: totalCount, content: .placeholder(id: id, timestamp: timestamp)))
|
||||||
}
|
}
|
||||||
totalCount += 1
|
totalCount += 1
|
||||||
}
|
}
|
||||||
|
sparseIndex += 1
|
||||||
|
case let .range(count):
|
||||||
|
if sparseIndex >= topItemCount {
|
||||||
|
totalCount += count
|
||||||
|
} else {
|
||||||
|
let overflowCount = sparseIndex + count - topItemCount
|
||||||
|
if overflowCount > 0 {
|
||||||
|
totalCount += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sparseIndex += count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.statePromise.set(.single(SparseMessageList.State(
|
self.statePromise.set(.single(SparseMessageList.State(
|
||||||
items: items,
|
items: items,
|
||||||
totalCount: items.count,
|
totalCount: totalCount,
|
||||||
isLoading: self.topSection == nil
|
isLoading: self.topSection == nil
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@ -307,7 +568,7 @@ public final class SparseMessageList {
|
|||||||
public struct State {
|
public struct State {
|
||||||
public final class Item {
|
public final class Item {
|
||||||
public enum Content {
|
public enum Content {
|
||||||
case message(Message)
|
case message(message: Message, isLocal: Bool)
|
||||||
case placeholder(id: MessageId, timestamp: Int32)
|
case placeholder(id: MessageId, timestamp: Int32)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,6 +586,12 @@ public final class SparseMessageList {
|
|||||||
public var isLoading: Bool
|
public var isLoading: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum LoadHoleDirection {
|
||||||
|
case around
|
||||||
|
case earlier
|
||||||
|
case later
|
||||||
|
}
|
||||||
|
|
||||||
public var state: Signal<State, NoError> {
|
public var state: Signal<State, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
@ -351,9 +618,15 @@ public final class SparseMessageList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadPlaceholders(ids: [MessageId]) {
|
/*public func loadPlaceholders(ids: [MessageId]) {
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
impl.loadPlaceholders(ids: ids)
|
impl.loadPlaceholders(ids: ids)
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public func loadHole(anchor: MessageId, direction: LoadHoleDirection) {
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.loadHole(anchor: anchor, direction: direction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -488,7 +488,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
|
|||||||
switch participantResult {
|
switch participantResult {
|
||||||
case let.channelParticipant(participant, _, _):
|
case let.channelParticipant(participant, _, _):
|
||||||
switch participant {
|
switch participant {
|
||||||
case let .channelParticipantSelf(_, inviterId, _):
|
case let .channelParticipantSelf(_, _, inviterId, _):
|
||||||
invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId))
|
invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 {
|
|||||||
case navigationShareIcon
|
case navigationShareIcon
|
||||||
case navigationSearchIcon
|
case navigationSearchIcon
|
||||||
case navigationCompactSearchIcon
|
case navigationCompactSearchIcon
|
||||||
|
case navigationCalendarIcon
|
||||||
case navigationMoreIcon
|
case navigationMoreIcon
|
||||||
case navigationAddIcon
|
case navigationAddIcon
|
||||||
case navigationPlayerCloseButton
|
case navigationPlayerCloseButton
|
||||||
|
@ -74,6 +74,12 @@ public struct PresentationResourcesRootController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func navigationCalendarIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
|
return theme.image(PresentationResourceKey.navigationCalendarIcon.rawValue, { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/Calendar"), color: theme.rootController.navigationBar.accentTextColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public static func navigationMoreIcon(_ theme: PresentationTheme) -> UIImage? {
|
public static func navigationMoreIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.navigationMoreIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.navigationMoreIcon.rawValue, { theme in
|
||||||
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
||||||
|
@ -242,6 +242,7 @@ swift_library(
|
|||||||
"//submodules/ComponentFlow:ComponentFlow",
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
"//submodules/AdUI:AdUI",
|
"//submodules/AdUI:AdUI",
|
||||||
"//submodules/SparseItemGrid:SparseItemGrid",
|
"//submodules/SparseItemGrid:SparseItemGrid",
|
||||||
|
"//submodules/CalendarMessageScreen:CalendarMessageScreen",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
},
|
},
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"provides-namespace" : true
|
"provides-namespace" : true
|
||||||
|
@ -565,9 +565,11 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class VisualMediaItem {
|
private final class VisualMediaItem {
|
||||||
|
let index: Int
|
||||||
let id: MessageId
|
let id: MessageId
|
||||||
let timestamp: Int32
|
let timestamp: Int32
|
||||||
let message: Message?
|
let message: Message?
|
||||||
|
let isLocal: Bool
|
||||||
|
|
||||||
enum StableId: Hashable {
|
enum StableId: Hashable {
|
||||||
case message(UInt32)
|
case message(UInt32)
|
||||||
@ -582,16 +584,109 @@ private final class VisualMediaItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(message: Message) {
|
init(index: Int, message: Message, isLocal: Bool) {
|
||||||
|
self.index = index
|
||||||
self.message = message
|
self.message = message
|
||||||
self.id = message.id
|
self.id = message.id
|
||||||
self.timestamp = message.timestamp
|
self.timestamp = message.timestamp
|
||||||
|
self.isLocal = isLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: MessageId, timestamp: Int32) {
|
init(index: Int, id: MessageId, timestamp: Int32) {
|
||||||
|
self.index = index
|
||||||
self.id = id
|
self.id = id
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.message = nil
|
self.message = nil
|
||||||
|
self.isLocal = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VisualMediaItemCollection {
|
||||||
|
var items: [VisualMediaItem]
|
||||||
|
var totalCount: Int
|
||||||
|
|
||||||
|
func item(at index: Int) -> VisualMediaItem? {
|
||||||
|
func binarySearch<A, T: Comparable>(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? {
|
||||||
|
var lowerIndex = 0
|
||||||
|
var upperIndex = inputArr.count - 1
|
||||||
|
|
||||||
|
if lowerIndex > upperIndex {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
while true {
|
||||||
|
let currentIndex = (lowerIndex + upperIndex) / 2
|
||||||
|
let value = extract(inputArr[currentIndex])
|
||||||
|
|
||||||
|
if value == searchItem {
|
||||||
|
return currentIndex
|
||||||
|
} else if lowerIndex > upperIndex {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
if (value > searchItem) {
|
||||||
|
upperIndex = currentIndex - 1
|
||||||
|
} else {
|
||||||
|
lowerIndex = currentIndex + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let itemIndex = binarySearch(self.items, extract: \.index, searchItem: index) {
|
||||||
|
return self.items[itemIndex]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func closestHole(at index: Int) -> (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)? {
|
||||||
|
var minDistance: Int?
|
||||||
|
for i in 0 ..< self.items.count {
|
||||||
|
if self.items[i].isLocal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let minDistanceValue = minDistance {
|
||||||
|
if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) {
|
||||||
|
minDistance = i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
minDistance = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let minDistance = minDistance {
|
||||||
|
let distance = index - self.items[minDistance].index
|
||||||
|
if abs(distance) <= 2 {
|
||||||
|
return (self.items[minDistance].id, .around)
|
||||||
|
} else if distance < 0 {
|
||||||
|
return (self.items[minDistance].id, .earlier)
|
||||||
|
} else {
|
||||||
|
return (self.items[minDistance].id, .later)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func closestItem(at index: Int) -> VisualMediaItem? {
|
||||||
|
if let item = self.item(at: index) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
var minDistance: Int?
|
||||||
|
for i in 0 ..< self.items.count {
|
||||||
|
if self.items[i].isLocal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let minDistanceValue = minDistance {
|
||||||
|
if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) {
|
||||||
|
minDistance = i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
minDistance = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let minDistance = minDistance {
|
||||||
|
return self.items[minDistance]
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,7 +847,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
|
|
||||||
private let listDisposable = MetaDisposable()
|
private let listDisposable = MetaDisposable()
|
||||||
private var hiddenMediaDisposable: Disposable?
|
private var hiddenMediaDisposable: Disposable?
|
||||||
private var mediaItems: [VisualMediaItem] = []
|
private var mediaItems = VisualMediaItemCollection(items: [], totalCount: 0)
|
||||||
private var itemsLayout: ItemsLayout?
|
private var itemsLayout: ItemsLayout?
|
||||||
private var visibleMediaItems: [VisualMediaItem.StableId: VisualMediaItemNode] = [:]
|
private var visibleMediaItems: [VisualMediaItem.StableId: VisualMediaItemNode] = [:]
|
||||||
|
|
||||||
@ -851,7 +946,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
|
|
||||||
func ensureMessageIsVisible(id: MessageId) {
|
func ensureMessageIsVisible(id: MessageId) {
|
||||||
let activeRect = self.scrollNode.bounds
|
let activeRect = self.scrollNode.bounds
|
||||||
for item in self.mediaItems {
|
for item in self.mediaItems.items {
|
||||||
if let message = item.message, message.id == id {
|
if let message = item.message, message.id == id {
|
||||||
if let itemNode = self.visibleMediaItems[item.stableId] {
|
if let itemNode = self.visibleMediaItems[item.stableId] {
|
||||||
if !activeRect.contains(itemNode.frame) {
|
if !activeRect.contains(itemNode.frame) {
|
||||||
@ -880,15 +975,13 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateHistory(list: SparseMessageList.State) {
|
private func updateHistory(list: SparseMessageList.State) {
|
||||||
//self.currentView = view
|
self.mediaItems = VisualMediaItemCollection(items: [], totalCount: list.totalCount)
|
||||||
|
|
||||||
self.mediaItems.removeAll()
|
|
||||||
for item in list.items {
|
for item in list.items {
|
||||||
switch item.content {
|
switch item.content {
|
||||||
case let .message(message):
|
case let .message(message, isLocal):
|
||||||
self.mediaItems.append(VisualMediaItem(message: message))
|
self.mediaItems.items.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal))
|
||||||
case let .placeholder(id, timestamp):
|
case let .placeholder(id, timestamp):
|
||||||
self.mediaItems.append(VisualMediaItem(id: id, timestamp: timestamp))
|
self.mediaItems.items.append(VisualMediaItem(index: item.index, id: id, timestamp: timestamp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.itemsLayout = nil
|
self.itemsLayout = nil
|
||||||
@ -915,7 +1008,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findLoadedMessage(id: MessageId) -> Message? {
|
func findLoadedMessage(id: MessageId) -> Message? {
|
||||||
for item in self.mediaItems {
|
for item in self.mediaItems.items {
|
||||||
if let message = item.message, message.id == id {
|
if let message = item.message, message.id == id {
|
||||||
return item.message
|
return item.message
|
||||||
}
|
}
|
||||||
@ -980,7 +1073,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||||
for item in self.mediaItems {
|
for item in self.mediaItems.items {
|
||||||
if let message = item.message, message.id == messageId {
|
if let message = item.message, message.id == messageId {
|
||||||
if let itemNode = self.visibleMediaItems[item.stableId] {
|
if let itemNode = self.visibleMediaItems[item.stableId] {
|
||||||
return itemNode.transitionNode()
|
return itemNode.transitionNode()
|
||||||
@ -1016,7 +1109,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
} else {
|
} else {
|
||||||
switch self.contentType {
|
switch self.contentType {
|
||||||
case .photoOrVideo, .gifs:
|
case .photoOrVideo, .gifs:
|
||||||
itemsLayout = .grid(ItemsLayout.Grid(containerWidth: availableWidth, itemCount: self.mediaItems.count, bottomInset: bottomInset))
|
itemsLayout = .grid(ItemsLayout.Grid(containerWidth: availableWidth, itemCount: self.mediaItems.totalCount, bottomInset: bottomInset))
|
||||||
}
|
}
|
||||||
self.itemsLayout = itemsLayout
|
self.itemsLayout = itemsLayout
|
||||||
}
|
}
|
||||||
@ -1104,7 +1197,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: currentParams.sideInset)
|
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: currentParams.sideInset)
|
||||||
|
|
||||||
if headerItem == nil && itemFrame.maxY > headerItemMinY {
|
if headerItem == nil && itemFrame.maxY > headerItemMinY {
|
||||||
headerItem = self.mediaItems[i].timestamp
|
if let item = self.mediaItems.closestItem(at: i) {
|
||||||
|
headerItem = item.timestamp
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1129,6 +1224,52 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func currentTopTimestamp() -> Int32? {
|
||||||
|
guard let currentParams = self.currentParams, let itemsLayout = self.itemsLayout else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerItemMinY = self.scrollNode.view.bounds.minY + 20.0
|
||||||
|
let activeRect = self.scrollNode.view.bounds
|
||||||
|
let visibleRect = activeRect.insetBy(dx: 0.0, dy: -400.0)
|
||||||
|
|
||||||
|
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
||||||
|
|
||||||
|
var headerItem: Int32?
|
||||||
|
|
||||||
|
if minVisibleIndex <= maxVisibleIndex {
|
||||||
|
for i in minVisibleIndex ... maxVisibleIndex {
|
||||||
|
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: currentParams.sideInset)
|
||||||
|
|
||||||
|
if headerItem == nil && itemFrame.maxY > headerItemMinY {
|
||||||
|
if let item = self.mediaItems.closestItem(at: i) {
|
||||||
|
headerItem = item.timestamp
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollToTimestamp(timestamp: Int32) {
|
||||||
|
guard let currentParams = self.currentParams else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let itemsLayout = self.itemsLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for item in self.mediaItems.items {
|
||||||
|
if item.timestamp <= timestamp {
|
||||||
|
let frame = itemsLayout.frame(forItemAt: item.index, sideInset: currentParams.sideInset)
|
||||||
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: frame.minY), animated: false)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
||||||
guard let itemsLayout = self.itemsLayout else {
|
guard let itemsLayout = self.itemsLayout else {
|
||||||
return
|
return
|
||||||
@ -1140,20 +1281,40 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
let (minActuallyVisibleIndex, maxActuallyVisibleIndex) = itemsLayout.visibleRange(rect: activeRect)
|
let (minActuallyVisibleIndex, maxActuallyVisibleIndex) = itemsLayout.visibleRange(rect: activeRect)
|
||||||
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
||||||
|
|
||||||
var requestPlaceholderIds: [MessageId] = []
|
var requestHole: (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)?
|
||||||
|
|
||||||
var validIds = Set<VisualMediaItem.StableId>()
|
var validIds = Set<VisualMediaItem.StableId>()
|
||||||
if minVisibleIndex <= maxVisibleIndex {
|
if minVisibleIndex <= maxVisibleIndex {
|
||||||
for i in minVisibleIndex ... maxVisibleIndex {
|
for itemIndex in minVisibleIndex ... maxVisibleIndex {
|
||||||
let stableId = self.mediaItems[i].stableId
|
let maybeItem = self.mediaItems.item(at: itemIndex)
|
||||||
validIds.insert(stableId)
|
var findHole = false
|
||||||
|
if let item = maybeItem {
|
||||||
if self.mediaItems[i].message == nil && !self.requestedPlaceholderIds.contains(self.mediaItems[i].id) {
|
if item.message == nil {
|
||||||
requestPlaceholderIds.append(self.mediaItems[i].id)
|
findHole = true
|
||||||
self.requestedPlaceholderIds.insert(self.mediaItems[i].id)
|
}
|
||||||
|
} else {
|
||||||
|
findHole = true
|
||||||
|
}
|
||||||
|
if findHole {
|
||||||
|
if let hole = self.mediaItems.closestHole(at: itemIndex) {
|
||||||
|
if requestHole == nil {
|
||||||
|
requestHole = hole
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset)
|
guard let item = maybeItem else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let stableId = item.stableId
|
||||||
|
validIds.insert(stableId)
|
||||||
|
|
||||||
|
if item.message == nil && !self.requestedPlaceholderIds.contains(item.id) {
|
||||||
|
//requestPlaceholderIds.append(item.id)
|
||||||
|
self.requestedPlaceholderIds.insert(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemFrame = itemsLayout.frame(forItemAt: itemIndex, sideInset: sideInset)
|
||||||
|
|
||||||
let itemNode: VisualMediaItemNode
|
let itemNode: VisualMediaItemNode
|
||||||
if let current = self.visibleMediaItems[stableId] {
|
if let current = self.visibleMediaItems[stableId] {
|
||||||
@ -1166,10 +1327,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
itemNode.frame = itemFrame
|
itemNode.frame = itemFrame
|
||||||
|
|
||||||
var itemSynchronousLoad = false
|
var itemSynchronousLoad = false
|
||||||
if i >= minActuallyVisibleIndex && i <= maxActuallyVisibleIndex {
|
if itemIndex >= minActuallyVisibleIndex && itemIndex <= maxActuallyVisibleIndex {
|
||||||
itemSynchronousLoad = synchronousLoad
|
itemSynchronousLoad = synchronousLoad
|
||||||
}
|
}
|
||||||
itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: itemSynchronousLoad)
|
itemNode.update(size: itemFrame.size, item: item, theme: theme, synchronousLoad: itemSynchronousLoad)
|
||||||
itemNode.updateIsVisible(itemFrame.intersects(activeRect))
|
itemNode.updateIsVisible(itemFrame.intersects(activeRect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1185,8 +1346,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !requestPlaceholderIds.isEmpty {
|
if let requestHole = requestHole {
|
||||||
self.listSource.loadPlaceholders(ids: requestPlaceholderIds)
|
self.listSource.loadHole(anchor: requestHole.anchor, direction: requestHole.direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -983,6 +983,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
|
|||||||
text = presentationData.strings.Settings_EditPhoto
|
text = presentationData.strings.Settings_EditPhoto
|
||||||
case .editVideo:
|
case .editVideo:
|
||||||
text = presentationData.strings.Settings_EditVideo
|
text = presentationData.strings.Settings_EditVideo
|
||||||
|
case .calendar:
|
||||||
|
text = ""
|
||||||
|
icon = PresentationResourcesRootController.navigationCalendarIcon(presentationData.theme)
|
||||||
}
|
}
|
||||||
self.accessibilityLabel = text
|
self.accessibilityLabel = text
|
||||||
|
|
||||||
@ -1023,6 +1026,7 @@ enum PeerInfoHeaderNavigationButtonKey {
|
|||||||
case search
|
case search
|
||||||
case editPhoto
|
case editPhoto
|
||||||
case editVideo
|
case editVideo
|
||||||
|
case calendar
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PeerInfoHeaderNavigationButtonSpec: Equatable {
|
struct PeerInfoHeaderNavigationButtonSpec: Equatable {
|
||||||
|
@ -60,6 +60,7 @@ import ActionSheetPeerItem
|
|||||||
import TelegramCallsUI
|
import TelegramCallsUI
|
||||||
import PeerInfoAvatarListNode
|
import PeerInfoAvatarListNode
|
||||||
import PasswordSetupUI
|
import PasswordSetupUI
|
||||||
|
import CalendarMessageScreen
|
||||||
|
|
||||||
protocol PeerInfoScreenItem: AnyObject {
|
protocol PeerInfoScreenItem: AnyObject {
|
||||||
var id: AnyHashable { get }
|
var id: AnyHashable { get }
|
||||||
@ -2736,6 +2737,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
case .search:
|
case .search:
|
||||||
strongSelf.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
strongSelf.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
strongSelf.activateSearch()
|
strongSelf.activateSearch()
|
||||||
|
case .calendar:
|
||||||
|
strongSelf.openCalendarSearch()
|
||||||
case .editPhoto, .editVideo:
|
case .editPhoto, .editVideo:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -5930,6 +5933,29 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openCalendarSearch() {
|
||||||
|
var initialTimestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
|
||||||
|
if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode, let timestamp = pane.currentTopTimestamp() {
|
||||||
|
initialTimestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
self.controller?.push(CalendarMessageScreen(context: self.context, peerId: self.peerId, initialTimestamp: initialTimestamp, navigateToDay: { [weak self] c, timestamp in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
c.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let pane = strongSelf.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode else {
|
||||||
|
c.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pane.scrollToTimestamp(timestamp: timestamp)
|
||||||
|
|
||||||
|
c.dismiss()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func updatePresentationData(_ presentationData: PresentationData) {
|
func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
|
|
||||||
@ -6263,6 +6289,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
switch currentPaneKey {
|
switch currentPaneKey {
|
||||||
case .files, .music, .links, .members:
|
case .files, .music, .links, .members:
|
||||||
navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true))
|
navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true))
|
||||||
|
case .media:
|
||||||
|
navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .calendar, isForExpandedView: true))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user