diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 5d112940b9..84f7812f07 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -453,18 +453,7 @@ private func formSupportApplePay(_ paymentForm: BotPaymentForm) -> Bool { guard let nativeProvider = paymentForm.nativeProvider else { return false } - let applePayProviders = Set([ - "stripe", - "sberbank", - "yandex", - "privatbank", - "tranzzo", - "paymaster", - "smartglocal", - ]) - if !applePayProviders.contains(nativeProvider.name) { - return false - } + guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else { return false } diff --git a/submodules/CalendarMessageScreen/BUILD b/submodules/CalendarMessageScreen/BUILD new file mode 100644 index 0000000000..c6c2445155 --- /dev/null +++ b/submodules/CalendarMessageScreen/BUILD @@ -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", + ], +) diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift new file mode 100644 index 0000000000..de04b48629 --- /dev/null +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -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] = [:] + + 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() + 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() + + 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 + 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) + } +} diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 176289e648..4ff15f44b9 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -2693,6 +2693,13 @@ final class MessageHistoryTable: Table { return nil } } + + 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) { var count: Int = 0 diff --git a/submodules/Postbox/Sources/MessageHistoryTagsTable.swift b/submodules/Postbox/Sources/MessageHistoryTagsTable.swift index afa6092684..42d0a8f09c 100644 --- a/submodules/Postbox/Sources/MessageHistoryTagsTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTagsTable.swift @@ -84,7 +84,7 @@ class MessageHistoryTagsTable: Table { 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] = [] let key: ValueBoxKey if let index = index { @@ -96,7 +96,13 @@ class MessageHistoryTagsTable: Table { } else { 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)) return true }, limit: count) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 2378e693db..de690c0ae2 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -708,6 +708,15 @@ public final class Transaction { assert(!self.disposed) 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) -> Set { assert(!self.disposed) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 34106ddb17..5bf79f2dae 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -319,7 +319,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } private let dateIndicator: ComponentHostView + private let lineIndicator: ComponentHostView + private var indicatorPosition: CGFloat? + private var scrollIndicatorHeight: CGFloat? + private var dragGesture: DragGesture? public private(set) var isDragging: Bool = false @@ -370,6 +374,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { strongSelf.isDragging = true + strongSelf.updateLineIndicator(transition: transition) + if let scrollView = strongSelf.beginScrolling?() { strongSelf.draggingScrollView = scrollView 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)) strongSelf.isDragging = false + + strongSelf.updateLineIndicator(transition: transition) + strongSelf.updateActivityTimer() }, moved: { [weak self] relativeOffset in @@ -470,7 +479,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { let dateIndicatorTopPosition = topIndicatorInset 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 @@ -487,7 +497,15 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { 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( transition: .immediate, component: AnyComponent(RoundedRectangle( @@ -497,7 +515,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { 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() { @@ -508,7 +526,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) } 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 { return } @@ -522,7 +540,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.dateIndicator.frame.contains(point) { - return self.view + return super.hitTest(point, with: event) } return nil diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index a2fdccf113..caeb5121bc 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -291,7 +291,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) } dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($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[885242707] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($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[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($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[1244130093] = { return Api.StatsGraph.parse_statsGraphAsync($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[547062491] = { return Api.ChannelLocation.parse_channelLocation($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[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) } dict[-478701471] = { return Api.account.ResetPasswordResult.parse_resetPasswordFailedWait($0) } @@ -1235,6 +1237,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.SecurePlainData: _1.serialize(buffer, boxed) + case let _1 as Api.SearchResultsPosition: + _1.serialize(buffer, boxed) case let _1 as Api.messages.AffectedHistory: _1.serialize(buffer, boxed) case let _1 as Api.StatsGraph: @@ -1335,6 +1339,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageRange: _1.serialize(buffer, boxed) + case let _1 as Api.messages.SearchResultsPositions: + _1.serialize(buffer, boxed) case let _1 as Api.messages.StickerSetInstallResult: _1.serialize(buffer, boxed) case let _1 as Api.account.ResetPasswordResult: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 1b8a1ae174..9659340ead 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -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 { case stickerSetInstallResultSuccess diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 70a407fd93..ff49814570 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -7617,7 +7617,7 @@ public extension Api { } public enum ChannelParticipant: TypeConstructorDescription { 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 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) @@ -7632,10 +7632,11 @@ public extension Api { serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) break - case .channelParticipantSelf(let userId, let inviterId, let date): + case .channelParticipantSelf(let flags, let userId, let inviterId, let date): if boxed { - buffer.appendInt32(682146919) + buffer.appendInt32(900251559) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt64(inviterId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) @@ -7684,8 +7685,8 @@ public extension Api { switch self { case .channelParticipant(let userId, let date): return ("channelParticipant", [("userId", userId), ("date", date)]) - case .channelParticipantSelf(let userId, let inviterId, let date): - return ("channelParticipantSelf", [("userId", userId), ("inviterId", inviterId), ("date", date)]) + case .channelParticipantSelf(let flags, let userId, let inviterId, let date): + return ("channelParticipantSelf", [("flags", flags), ("userId", userId), ("inviterId", inviterId), ("date", date)]) case .channelParticipantCreator(let flags, let userId, let adminRights, let 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): @@ -7712,17 +7713,20 @@ public extension Api { } } public static func parse_channelParticipantSelf(_ reader: BufferReader) -> ChannelParticipant? { - var _1: Int64? - _1 = reader.readInt64() + var _1: Int32? + _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.ChannelParticipant.channelParticipantSelf(userId: _1!, inviterId: _2!, date: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ChannelParticipant.channelParticipantSelf(flags: _1!, userId: _2!, inviterId: _3!, date: _4!) } else { 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 { case statsGraphAsync(token: String) diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 3cc98b3729..4df54958c7 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -4484,6 +4484,23 @@ public extension Api { }) } + public static func getSearchResultsPositions(peer: Api.InputPeer, filter: Api.MessagesFilter, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + 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) { let buffer = Buffer() buffer.appendInt32(2145904661) diff --git a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift index d8622dc414..64f93c7ed6 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift @@ -207,7 +207,7 @@ extension ChannelParticipant { self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil) 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) - 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) case let .channelParticipantLeft(userId): self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil) diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 24a1d0ced5..61f25a6b04 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -158,6 +158,7 @@ struct FetchMessageHistoryHoleResult: Equatable { var strictRemovedIndices: IndexSet var actualPeerId: PeerId? 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 { @@ -183,7 +184,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } |> mapToSignal { (inputPeer, hash) -> Signal in 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)") @@ -501,6 +502,23 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH 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 { filledRange = minMaxRange strictFilledIndices = IndexSet() @@ -561,7 +579,8 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)), strictRemovedIndices: strictFilledIndices, actualPeerId: storeMessages.first?.id.peerId, - actualThreadId: storeMessages.first?.threadId + actualThreadId: storeMessages.first?.threadId, + ids: fullIds ) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index 9f6071c85e..6fc4f4e1fb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -779,7 +779,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa let preloadedHistory = preloadedHistoryPosition |> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in 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 if let threadMessageId = threadMessageId { @@ -832,7 +832,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa 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)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index f334c9ed8a..0e869cca8a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -28,17 +28,61 @@ public final class SparseMessageList { } } - private struct ItemIndices: Equatable { - var ids: [MessageId] - var timestamps: [Int32] + private struct SparseItems: Equatable { + enum Item: Equatable { + 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 topSection: TopSection? private var topItemsDisposable: Disposable? - private var messageIndices: ItemIndices? - private var messageIndicesDisposable: Disposable? + private var sparseItems: SparseItems? + 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 loadedPlaceholders: [MessageId: Message] = [:] @@ -52,31 +96,67 @@ public final class SparseMessageList { self.messageTag = messageTag 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) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal in 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)) - |> map { result -> ItemIndices in + return account.network.request(Api.functions.messages.getSearchResultsPositions(peer: inputPeer, filter: .inputMessagesFilterPhotoVideo, offsetId: 0, limit: 1000)) + |> map { result -> SparseItems in switch result { - case let .searchResultsRawMessages(msgIds, msgDates): - return ItemIndices(ids: msgIds.map { id in - return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id) - }, timestamps: msgDates) + case let .searchResultsPositions(totalCount, positions): + struct Position { + var id: Int32 + 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 + } + } + }).map { position -> Position in + switch position { + case let .searchResultPosition(id, date, offset): + return Position(id: id, date: date, offset: Int(offset)) + } + } + + 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 in - return .single(ItemIndices(ids: [], timestamps: [])) + |> `catch` { _ -> Signal in + return .single(SparseItems(items: [])) } } - |> deliverOnMainQueue).start(next: { [weak self] indices in + |> deliverOnMainQueue).start(next: { [weak self] sparseItems in guard let strongSelf = self else { return } - strongSelf.messageIndices = indices + strongSelf.sparseItems = sparseItems if strongSelf.topSection != nil { strongSelf.updateState() } @@ -85,11 +165,12 @@ public final class SparseMessageList { deinit { self.topItemsDisposable?.dispose() - self.messageIndicesDisposable?.dispose() + self.sparseItemsDisposable?.dispose() + self.loadHoleDisposable.dispose() } 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 guard let strongSelf = self else { return @@ -240,18 +321,181 @@ public final class SparseMessageList { for message in messages { 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() }) } + 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) { var topSection: TopSection? if view.isLoading { topSection = nil } else { - topSection = TopSection(messages: view.entries.map { entry in + topSection = TopSection(messages: view.entries.lazy.reversed().map { entry in return entry.message }) } @@ -268,7 +512,7 @@ public final class SparseMessageList { if let topSection = self.topSection { for i in 0 ..< topSection.messages.count { 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 message.id < minMessageIdValue { minMessageId = message.id @@ -279,23 +523,40 @@ public final class SparseMessageList { } } + let topItemCount = items.count var totalCount = items.count - if let minMessageId = minMessageId, let messageIndices = self.messageIndices { - for i in 0 ..< messageIndices.ids.count { - if messageIndices.ids[i] < minMessageId { - if let message = self.loadedPlaceholders[messageIndices.ids[i]] { - items.append(SparseMessageList.State.Item(index: items.count, content: .message(message))) - } else { - items.append(SparseMessageList.State.Item(index: items.count, content: .placeholder(id: messageIndices.ids[i], timestamp: messageIndices.timestamps[i]))) + if let minMessageId = minMessageId, let sparseItems = self.sparseItems { + var sparseIndex = 0 + let _ = minMessageId + for i in 0 ..< sparseItems.items.count { + 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 { + 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( items: items, - totalCount: items.count, + totalCount: totalCount, isLoading: self.topSection == nil ))) } @@ -307,7 +568,7 @@ public final class SparseMessageList { public struct State { public final class Item { public enum Content { - case message(Message) + case message(message: Message, isLocal: Bool) case placeholder(id: MessageId, timestamp: Int32) } @@ -325,6 +586,12 @@ public final class SparseMessageList { public var isLoading: Bool } + public enum LoadHoleDirection { + case around + case earlier + case later + } + public var state: Signal { return Signal { subscriber in 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 impl.loadPlaceholders(ids: ids) } + }*/ + + public func loadHole(anchor: MessageId, direction: LoadHoleDirection) { + self.impl.with { impl in + impl.loadHole(anchor: anchor, direction: direction) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 1def6650bf..a1c7a2702f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -488,7 +488,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch participantResult { case let.channelParticipant(participant, _, _): switch participant { - case let .channelParticipantSelf(_, inviterId, _): + case let .channelParticipantSelf(_, _, inviterId, _): invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId)) default: break diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 04a74a4cd9..d751f337de 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 { case navigationShareIcon case navigationSearchIcon case navigationCompactSearchIcon + case navigationCalendarIcon case navigationMoreIcon case navigationAddIcon case navigationPlayerCloseButton diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index a054f36ae8..e0eda055ff 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -73,6 +73,12 @@ public struct PresentationResourcesRootController { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor) }) } + + 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? { return theme.image(PresentationResourceKey.navigationMoreIcon.rawValue, { theme in diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 13fbcab13e..07b5c65e3c 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -242,6 +242,7 @@ swift_library( "//submodules/ComponentFlow:ComponentFlow", "//submodules/AdUI:AdUI", "//submodules/SparseItemGrid:SparseItemGrid", + "//submodules/CalendarMessageScreen:CalendarMessageScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Contents.json index 38f0c81fc2..6e965652df 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat List/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 99f27ffd4e..5b9ede0bc1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -565,9 +565,11 @@ private final class VisualMediaItemNode: ASDisplayNode { } private final class VisualMediaItem { + let index: Int let id: MessageId let timestamp: Int32 let message: Message? + let isLocal: Bool enum StableId: Hashable { 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.id = message.id 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.timestamp = timestamp self.message = nil + self.isLocal = false + } +} + +private struct VisualMediaItemCollection { + var items: [VisualMediaItem] + var totalCount: Int + + func item(at index: Int) -> VisualMediaItem? { + func binarySearch(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? { + var lowerIndex = 0 + var upperIndex = inputArr.count - 1 + + if lowerIndex > upperIndex { + return nil + } + + while true { + let currentIndex = (lowerIndex + upperIndex) / 2 + let value = extract(inputArr[currentIndex]) + + if value == searchItem { + return currentIndex + } else if lowerIndex > upperIndex { + return nil + } else { + if (value > searchItem) { + upperIndex = currentIndex - 1 + } else { + lowerIndex = currentIndex + 1 + } + } + } + } + + if let itemIndex = binarySearch(self.items, extract: \.index, searchItem: index) { + return self.items[itemIndex] + } + return nil + } + + func 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 var hiddenMediaDisposable: Disposable? - private var mediaItems: [VisualMediaItem] = [] + private var mediaItems = VisualMediaItemCollection(items: [], totalCount: 0) private var itemsLayout: ItemsLayout? private var visibleMediaItems: [VisualMediaItem.StableId: VisualMediaItemNode] = [:] @@ -851,7 +946,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro func ensureMessageIsVisible(id: MessageId) { 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 itemNode = self.visibleMediaItems[item.stableId] { if !activeRect.contains(itemNode.frame) { @@ -880,15 +975,13 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } private func updateHistory(list: SparseMessageList.State) { - //self.currentView = view - - self.mediaItems.removeAll() + self.mediaItems = VisualMediaItemCollection(items: [], totalCount: list.totalCount) for item in list.items { switch item.content { - case let .message(message): - self.mediaItems.append(VisualMediaItem(message: message)) + case let .message(message, isLocal): + self.mediaItems.items.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal)) 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 @@ -915,7 +1008,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func findLoadedMessage(id: MessageId) -> Message? { - for item in self.mediaItems { + for item in self.mediaItems.items { if let message = item.message, message.id == id { return item.message } @@ -980,7 +1073,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } 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 itemNode = self.visibleMediaItems[item.stableId] { return itemNode.transitionNode() @@ -1016,7 +1109,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } else { switch self.contentType { 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 } @@ -1104,7 +1197,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: currentParams.sideInset) if headerItem == nil && itemFrame.maxY > headerItemMinY { - headerItem = self.mediaItems[i].timestamp + if let item = self.mediaItems.closestItem(at: i) { + headerItem = item.timestamp + } break } } @@ -1128,6 +1223,52 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro transition: transition ) } + + 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) { guard let itemsLayout = self.itemsLayout else { @@ -1140,20 +1281,40 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro let (minActuallyVisibleIndex, maxActuallyVisibleIndex) = itemsLayout.visibleRange(rect: activeRect) let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect) - var requestPlaceholderIds: [MessageId] = [] + var requestHole: (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)? var validIds = Set() if minVisibleIndex <= maxVisibleIndex { - for i in minVisibleIndex ... maxVisibleIndex { - let stableId = self.mediaItems[i].stableId + for itemIndex in minVisibleIndex ... maxVisibleIndex { + let maybeItem = self.mediaItems.item(at: itemIndex) + var findHole = false + if let item = maybeItem { + if item.message == nil { + findHole = true + } + } else { + findHole = true + } + if findHole { + if let hole = self.mediaItems.closestHole(at: itemIndex) { + if requestHole == nil { + requestHole = hole + } + } + } + + guard let item = maybeItem else { + continue + } + let stableId = item.stableId validIds.insert(stableId) - if self.mediaItems[i].message == nil && !self.requestedPlaceholderIds.contains(self.mediaItems[i].id) { - requestPlaceholderIds.append(self.mediaItems[i].id) - self.requestedPlaceholderIds.insert(self.mediaItems[i].id) + if item.message == nil && !self.requestedPlaceholderIds.contains(item.id) { + //requestPlaceholderIds.append(item.id) + self.requestedPlaceholderIds.insert(item.id) } - let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset) + let itemFrame = itemsLayout.frame(forItemAt: itemIndex, sideInset: sideInset) let itemNode: VisualMediaItemNode if let current = self.visibleMediaItems[stableId] { @@ -1166,10 +1327,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro itemNode.frame = itemFrame var itemSynchronousLoad = false - if i >= minActuallyVisibleIndex && i <= maxActuallyVisibleIndex { + if itemIndex >= minActuallyVisibleIndex && itemIndex <= maxActuallyVisibleIndex { 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)) } } @@ -1185,8 +1346,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } } - if !requestPlaceholderIds.isEmpty { - self.listSource.loadPlaceholders(ids: requestPlaceholderIds) + if let requestHole = requestHole { + self.listSource.loadHole(anchor: requestHole.anchor, direction: requestHole.direction) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 94f04a0789..b651e9f429 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -983,6 +983,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { text = presentationData.strings.Settings_EditPhoto case .editVideo: text = presentationData.strings.Settings_EditVideo + case .calendar: + text = "" + icon = PresentationResourcesRootController.navigationCalendarIcon(presentationData.theme) } self.accessibilityLabel = text @@ -1023,6 +1026,7 @@ enum PeerInfoHeaderNavigationButtonKey { case search case editPhoto case editVideo + case calendar } struct PeerInfoHeaderNavigationButtonSpec: Equatable { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1ec09db815..d38cf35d23 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -60,6 +60,7 @@ import ActionSheetPeerItem import TelegramCallsUI import PeerInfoAvatarListNode import PasswordSetupUI +import CalendarMessageScreen protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -2736,6 +2737,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD case .search: strongSelf.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) strongSelf.activateSearch() + case .calendar: + strongSelf.openCalendarSearch() case .editPhoto, .editVideo: break } @@ -5929,6 +5932,29 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD transition.updateAlpha(node: navigationBar, alpha: 1.0) } } + + 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) { self.presentationData = presentationData @@ -6263,6 +6289,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD switch currentPaneKey { case .files, .music, .links, .members: navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) + case .media: + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .calendar, isForExpandedView: true)) default: break }