diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 467627a1bc..d5ac4639b4 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -153,6 +153,7 @@ private final class ImageCache: Equatable { private struct FilledCircle: Hashable { var diameter: CGFloat + var innerDiameter: CGFloat? var color: UInt32 } @@ -163,10 +164,17 @@ private final class ImageCache: Equatable { var string: String } + private struct MonthSelection: Hashable { + var leftRadius: CGFloat + var rightRadius: CGFloat + var maxRadius: CGFloat + var color: UInt32 + } + private var items: [AnyHashable: UIImage] = [:] - func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage { - let key = AnyHashable(FilledCircle(diameter: diameter, color: color.argb)) + func filledCircle(diameter: CGFloat, innerDiameter: CGFloat?, color: UIColor) -> UIImage { + let key = AnyHashable(FilledCircle(diameter: diameter, innerDiameter: innerDiameter, color: color.argb)) if let image = self.items[key] { return image } @@ -176,6 +184,12 @@ private final class ImageCache: Equatable { context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + if let innerDiameter = innerDiameter { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) / 2.0, y: (size.height - innerDiameter) / 2.0), size: CGSize(width: innerDiameter, height: innerDiameter))) + } })!.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) self.items[key] = image return image @@ -194,7 +208,10 @@ private final class ImageCache: Equatable { font = Font.regular(fontSize) } let attributedString = NSAttributedString(string: string, font: font, textColor: color) - let rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil) + var rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil) + if string == "1" { + rect.origin.x -= 1.0 + } let image = generateImage(CGSize(width: ceil(rect.width), height: ceil(rect.height)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -205,17 +222,91 @@ private final class ImageCache: Equatable { self.items[key] = image return image } + + func monthSelection(leftRadius: CGFloat, rightRadius: CGFloat, maxRadius: CGFloat, color: UIColor) -> UIImage { + let key = AnyHashable(MonthSelection(leftRadius: leftRadius, rightRadius: rightRadius, maxRadius: maxRadius, color: color.argb)) + if let image = self.items[key] { + return image + } + + let image = generateImage(CGSize(width: maxRadius, height: maxRadius), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + UIGraphicsPushContext(context) + + context.clip(to: CGRect(origin: CGPoint(), size: CGSize(width: size.width / 2.0, height: size.height))) + UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: leftRadius).fill() + + context.resetClip() + context.clip(to: CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width - size.width / 2.0, height: size.height))) + UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: rightRadius).fill() + + UIGraphicsPopContext() + })!.stretchableImage(withLeftCapWidth: Int(maxRadius / 2.0), topCapHeight: Int(maxRadius / 2.0)) + self.items[key] = image + return image + } +} + +private final class ImageComponent: Component { + let image: UIImage? + + init( + image: UIImage? + ) { + self.image = image + } + + static func ==(lhs: ImageComponent, rhs: ImageComponent) -> Bool { + if lhs.image !== rhs.image { + return false + } + return true + } + + final class View: UIImageView { + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: ImageComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + self.image = component.image + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } } private final class DayComponent: Component { typealias EnvironmentType = ImageCache + enum DaySelection { + case none + case edge + case middle + } + let title: String let isCurrent: Bool let isEnabled: Bool let theme: PresentationTheme let context: AccountContext let media: DayMedia? + let selection: DaySelection + let isSelecting: Bool let action: () -> Void init( @@ -225,6 +316,8 @@ private final class DayComponent: Component { theme: PresentationTheme, context: AccountContext, media: DayMedia?, + selection: DaySelection, + isSelecting: Bool, action: @escaping () -> Void ) { self.title = title @@ -233,6 +326,8 @@ private final class DayComponent: Component { self.theme = theme self.context = context self.media = media + self.selection = selection + self.isSelecting = isSelecting self.action = action } @@ -255,13 +350,20 @@ private final class DayComponent: Component { if lhs.media != rhs.media { return false } + if lhs.selection != rhs.selection { + return false + } + if lhs.isSelecting != rhs.isSelecting { + return false + } return true } final class View: UIView { - private let button: HighlightableButton + private let button: HighlightTrackingButton private let highlightView: UIImageView + private var selectionView: UIImageView? private let titleView: UIImageView private var mediaPreviewView: MediaPreviewView? @@ -269,9 +371,10 @@ private final class DayComponent: Component { private var currentMedia: DayMedia? private(set) var index: MessageIndex? + private var isHighlightingEnabled: Bool = false init() { - self.button = HighlightableButton() + self.button = HighlightTrackingButton() self.highlightView = UIImageView() self.highlightView.isUserInteractionEnabled = false self.titleView = UIImageView() @@ -285,6 +388,17 @@ private final class DayComponent: Component { self.addSubview(self.button) self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.button.highligthedChanged = { [weak self] highligthed in + guard let strongSelf = self, let mediaPreviewView = strongSelf.mediaPreviewView else { + return + } + if strongSelf.isHighlightingEnabled && highligthed { + mediaPreviewView.alpha = 0.8 + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(layer: mediaPreviewView.layer, alpha: 1.0) + } + } } required init?(coder aDecoder: NSCoder) { @@ -298,15 +412,14 @@ private final class DayComponent: Component { func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.action = component.action self.index = component.media?.message.index + self.isHighlightingEnabled = component.isEnabled && component.media != nil && !component.isSelecting let diameter = min(availableSize.width, availableSize.height) let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) let imageCache = environment[ImageCache.self] if component.media != nil { - self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2)) - } else if component.isCurrent { - self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor) + self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, innerDiameter: nil, color: UIColor(white: 0.0, alpha: 0.2)) } else { self.highlightView.image = nil } @@ -330,34 +443,86 @@ private final class DayComponent: Component { let titleColor: UIColor let titleFontSize: CGFloat let titleFontIsSemibold: Bool - if component.isCurrent || component.media != nil { - titleColor = component.theme.list.itemCheckColors.foregroundColor + if component.media != nil { + if component.theme.overallDarkAppearance { + titleColor = component.theme.list.itemPrimaryTextColor + } else { + titleColor = component.theme.list.itemCheckColors.foregroundColor + } titleFontSize = 17.0 titleFontIsSemibold = true - } else if component.isEnabled { - titleColor = component.theme.list.itemPrimaryTextColor - titleFontSize = 17.0 - titleFontIsSemibold = false } else { - titleColor = component.theme.list.itemDisabledTextColor titleFontSize = 17.0 - titleFontIsSemibold = false + switch component.selection { + case .middle, .edge: + titleFontIsSemibold = true + default: + titleFontIsSemibold = component.isCurrent + } + + if case .edge = component.selection { + if component.theme.overallDarkAppearance { + titleColor = component.theme.list.itemPrimaryTextColor + } else { + titleColor = component.theme.list.itemCheckColors.foregroundColor + } + } else { + if component.isCurrent { + titleColor = component.theme.list.itemAccentColor + } else if component.isEnabled { + titleColor = component.theme.list.itemPrimaryTextColor + } else { + titleColor = component.theme.list.itemDisabledTextColor + } + } + } + + switch component.selection { + case .edge: + let selectionView: UIImageView + if let current = self.selectionView { + selectionView = current + } else { + selectionView = UIImageView() + self.selectionView = selectionView + self.button.insertSubview(selectionView, belowSubview: self.titleView) + } + selectionView.frame = contentFrame + if self.mediaPreviewView != nil { + selectionView.image = imageCache.value.filledCircle(diameter: diameter, innerDiameter: diameter - 2.0 * 2.0, color: component.theme.list.itemCheckColors.fillColor) + } else { + selectionView.image = imageCache.value.filledCircle(diameter: diameter, innerDiameter: nil, color: component.theme.list.itemCheckColors.fillColor) + } + case .middle, .none: + if let selectionView = self.selectionView { + self.selectionView = nil + selectionView.removeFromSuperview() + } + } + + let contentScale: CGFloat + switch component.selection { + case .edge, .middle: + contentScale = (contentFrame.width - 8.0) / contentFrame.width + case .none: + contentScale = 1.0 } let titleImage = imageCache.value.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title) self.titleView.image = titleImage let titleSize = titleImage.size - transition.setFrame(view: self.highlightView, frame: contentFrame) + transition.setFrame(view: self.highlightView, frame: CGRect(origin: CGPoint(x: contentFrame.midX - contentFrame.width * contentScale / 2.0, y: contentFrame.midY - contentFrame.width * contentScale / 2.0), size: CGSize(width: contentFrame.width * contentScale, height: contentFrame.height * contentScale))) self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) self.button.frame = CGRect(origin: CGPoint(), size: availableSize) - self.button.isEnabled = component.isEnabled && component.media != nil if let mediaPreviewView = self.mediaPreviewView { mediaPreviewView.frame = contentFrame mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false) + + mediaPreviewView.layer.sublayerTransform = CATransform3DMakeScale(contentScale, contentScale, 1.0) } return availableSize @@ -381,7 +546,8 @@ private final class MonthComponent: CombinedComponent { let foregroundColor: UIColor let strings: PresentationStrings let theme: PresentationTheme - let navigateToDay: (Int32) -> Void + let dayAction: (Int32) -> Void + let selectedDays: ClosedRange? init( context: AccountContext, @@ -389,14 +555,16 @@ private final class MonthComponent: CombinedComponent { foregroundColor: UIColor, strings: PresentationStrings, theme: PresentationTheme, - navigateToDay: @escaping (Int32) -> Void + dayAction: @escaping (Int32) -> Void, + selectedDays: ClosedRange? ) { self.context = context self.model = model self.foregroundColor = foregroundColor self.strings = strings self.theme = theme - self.navigateToDay = navigateToDay + self.dayAction = dayAction + self.selectedDays = selectedDays } static func ==(lhs: MonthComponent, rhs: MonthComponent) -> Bool { @@ -415,6 +583,9 @@ private final class MonthComponent: CombinedComponent { if lhs.theme !== rhs.theme { return false } + if lhs.selectedDays != rhs.selectedDays { + return false + } return true } @@ -422,6 +593,7 @@ private final class MonthComponent: CombinedComponent { let title = Child(Text.self) let weekdayTitles = ChildMap(environment: Empty.self, keyedBy: Int.self) let days = ChildMap(environment: ImageCache.self, keyedBy: Int.self) + let selections = ChildMap(environment: Empty.self, keyedBy: Int.self) return { context in let sideInset: CGFloat = 14.0 @@ -437,7 +609,7 @@ private final class MonthComponent: CombinedComponent { 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 + color: context.component.foregroundColor ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), transition: .immediate @@ -448,7 +620,7 @@ private final class MonthComponent: CombinedComponent { component: AnyComponent(Text( text: dayName(index: index, strings: context.component.strings), font: Font.regular(10.0), - color: .black + color: context.component.foregroundColor )), availableSize: CGSize(width: 100.0, height: 100.0), transition: .immediate @@ -472,7 +644,18 @@ private final class MonthComponent: CombinedComponent { } let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(index) - let navigateToDay = context.component.navigateToDay + let dayAction = context.component.dayAction + + let daySelection: DayComponent.DaySelection + if let selectedDays = context.component.selectedDays, selectedDays.contains(dayTimestamp) { + if selectedDays.lowerBound == dayTimestamp || selectedDays.upperBound == dayTimestamp { + daySelection = .edge + } else { + daySelection = .middle + } + } else { + daySelection = .none + } return days[index].update( component: AnyComponent(DayComponent( @@ -482,8 +665,10 @@ private final class MonthComponent: CombinedComponent { theme: context.component.theme, context: context.component.context, media: context.component.model.mediaByDay[index], + selection: daySelection, + isSelecting: context.component.selectedDays != nil, action: { - navigateToDay(dayTimestamp) + dayAction(dayTimestamp) } )), environment: { @@ -515,10 +700,88 @@ private final class MonthComponent: CombinedComponent { let baseDayY = maxWeekdayY + weekdayDaySpacing var maxDayY = baseDayY + struct LineSelection { + var range: ClosedRange + var leftTimestamp: Int32 + var rightTimestamp: Int32 + } + + var selectionsByLine: [Int: LineSelection] = [:] + 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 rowIndex = gridIndex % 7 + let lineIndex = gridIndex / 7 + + if let selectedDays = context.component.selectedDays { + let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(i) + if selectedDays.contains(dayTimestamp) { + if var currentSelection = selectionsByLine[lineIndex] { + if rowIndex < currentSelection.range.lowerBound { + currentSelection.range = rowIndex ... currentSelection.range.upperBound + currentSelection.leftTimestamp = dayTimestamp + } else { + currentSelection.range = currentSelection.range.lowerBound ... rowIndex + currentSelection.rightTimestamp = dayTimestamp + } + selectionsByLine[lineIndex] = currentSelection + } else { + selectionsByLine[lineIndex] = LineSelection( + range: rowIndex ... rowIndex, + leftTimestamp: dayTimestamp, + rightTimestamp: dayTimestamp + ) + } + } + } + } + + if let selectedDays = context.component.selectedDays { + for (lineIndex, selection) in selectionsByLine { + let imageCache = context.environment[ImageCache.self] + + let dayItemSize = updatedDays[0].size + let deltaWidth = floor((weekdayWidth - dayItemSize.width) / 2.0) + let deltaHeight = floor((weekdaySize - dayItemSize.width) / 2.0) + let minX = sideInset + CGFloat(selection.range.lowerBound) * weekdayWidth + deltaWidth + let maxX = sideInset + CGFloat(selection.range.upperBound + 1) * weekdayWidth - deltaWidth + let minY = baseDayY + CGFloat(lineIndex) * (weekdaySize + weekdaySpacing) + deltaHeight + let maxY = minY + dayItemSize.width + + let leftRadius: CGFloat + if selectedDays.lowerBound == selection.leftTimestamp { + leftRadius = dayItemSize.width + } else { + leftRadius = 10.0 + } + let rightRadius: CGFloat + if selectedDays.upperBound == selection.rightTimestamp { + rightRadius = dayItemSize.width + } else { + rightRadius = 10.0 + } + + let monthSelectionColor = context.component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1) + + let selectionRect = CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY)) + let selection = selections[lineIndex].update( + component: AnyComponent(ImageComponent(image: imageCache.value.monthSelection(leftRadius: leftRadius, rightRadius: rightRadius, maxRadius: dayItemSize.width, color: monthSelectionColor))), + availableSize: selectionRect.size, + transition: .immediate + ) + context.add(selection + .position(CGPoint(x: selectionRect.midX, y: selectionRect.midY)) + ) + } + } + + for i in 0 ..< updatedDays.count { + let gridIndex = (context.component.model.firstDayWeekday - 1) + i + let rowIndex = gridIndex % 7 + let lineIndex = gridIndex / 7 + + let gridX = sideInset + CGFloat(rowIndex) * weekdayWidth + let gridY = baseDayY + CGFloat(lineIndex) * (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) @@ -602,6 +865,11 @@ private func monthMetadata(calendar: Calendar, for baseDate: Date, currentYear: public final class CalendarMessageScreen: ViewController { private final class Node: ViewControllerTracingNode, UIScrollViewDelegate { + struct SelectionState { + var dayRange: ClosedRange? + } + + private weak var controller: CalendarMessageScreen? private let context: AccountContext private let peerId: PeerId private let initialTimestamp: Int32 @@ -629,7 +897,13 @@ public final class CalendarMessageScreen: ViewController { private weak var currentGestureDayView: DayComponent.View? - init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void, previewDay: @escaping (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void) { + private var selectionToolbarNode: ToolbarNode? + private(set) var selectionState: SelectionState? + + private var ignoreContentOffset: Bool = false + + init(controller: CalendarMessageScreen, context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void, previewDay: @escaping (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void) { + self.controller = controller self.context = context self.peerId = peerId self.initialTimestamp = initialTimestamp @@ -652,6 +926,11 @@ public final class CalendarMessageScreen: ViewController { } self.scrollView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.scrollView.disablesInteractiveModalDismiss = true + if self.presentationData.theme.overallDarkAppearance { + self.scrollView.indicatorStyle = .white + } else { + self.scrollView.indicatorStyle = .black + } super.init() @@ -660,7 +939,7 @@ public final class CalendarMessageScreen: ViewController { return false } - guard let result = strongSelf.contextGestureContainerNode.view.hitTest(point, with: nil) as? HighlightableButton else { + guard let result = strongSelf.contextGestureContainerNode.view.hitTest(point, with: nil) as? UIButton else { return false } @@ -793,11 +1072,90 @@ public final class CalendarMessageScreen: ViewController { self.stateDisposable?.dispose() } + func toggleSelectionMode() { + if self.selectionState == nil { + self.selectionState = SelectionState(dayRange: nil) + } else { + self.selectionState = nil + } + + self.contextGestureContainerNode.isGestureEnabled = self.selectionState == nil + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring)) + } + } + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) - if self.updateScrollLayoutIfNeeded() { + var tabBarHeight: CGFloat + var options: ContainerViewLayoutInsetOptions = [] + if layout.metrics.widthClass == .regular { + options.insert(.input) + } + let bottomInset: CGFloat = layout.insets(options: options).bottom + if !layout.safeInsets.left.isZero { + tabBarHeight = 34.0 + bottomInset + } else { + tabBarHeight = 49.0 + bottomInset + } + + let tabBarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + + if let _ = self.selectionState { + let selectionToolbarNode: ToolbarNode + if let currrent = self.selectionToolbarNode { + selectionToolbarNode = currrent + + transition.updateFrame(node: selectionToolbarNode, frame: tabBarFrame) + selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: self.presentationData.strings.DialogList_ClearHistoryConfirmation, isEnabled: self.selectionState?.dayRange != nil, color: .custom(self.presentationData.theme.list.itemDestructiveColor))), transition: transition) + } else { + selectionToolbarNode = ToolbarNode( + theme: TabBarControllerTheme( + rootControllerTheme: self.presentationData.theme), + displaySeparator: true, + left: { + }, + right: { + }, + middle: { [weak self] in + self?.selectionToolbarActionSelected() + } + ) + selectionToolbarNode.frame = tabBarFrame + selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: self.presentationData.strings.DialogList_ClearHistoryConfirmation, isEnabled: self.selectionState?.dayRange != nil, color: .custom(self.presentationData.theme.list.itemDestructiveColor))), transition: .immediate) + self.addSubnode(selectionToolbarNode) + self.selectionToolbarNode = selectionToolbarNode + transition.animatePositionAdditive(node: selectionToolbarNode, offset: CGPoint(x: 0.0, y: tabBarFrame.height)) + } + } else if let selectionToolbarNode = self.selectionToolbarNode { + self.selectionToolbarNode = nil + transition.updatePosition(node: selectionToolbarNode, position: CGPoint(x: selectionToolbarNode.position.x, y: selectionToolbarNode.position.y + tabBarFrame.height), completion: { [weak selectionToolbarNode] _ in + selectionToolbarNode?.removeFromSupernode() + }) + } + + let _ = self.updateScrollLayoutIfNeeded() + + let previousInset = self.scrollView.contentInset.top + let updatedInset = self.selectionToolbarNode?.bounds.height ?? 0.0 + if previousInset != updatedInset { + let delta = updatedInset - previousInset + self.ignoreContentOffset = true + let contentOffset = self.scrollView.contentOffset + self.scrollView.contentInset = UIEdgeInsets(top: updatedInset, left: 0.0, bottom: 0.0, right: 0.0) + var updatedContentOffset = CGPoint(x: contentOffset.x, y: contentOffset.y - delta) + if updatedContentOffset.y > self.scrollView.contentSize.height - self.scrollView.bounds.height { + updatedContentOffset.y = self.scrollView.contentSize.height - self.scrollView.bounds.height + } + if updatedContentOffset.y < -self.scrollView.contentInset.top { + updatedContentOffset.y = -self.scrollView.contentInset.top + } + self.scrollView.contentOffset = updatedContentOffset + self.ignoreContentOffset = false + transition.animateOffsetAdditive(layer: self.scrollView.layer, offset: contentOffset.y - updatedContentOffset.y) } if isFirstLayout { @@ -813,7 +1171,7 @@ public final class CalendarMessageScreen: ViewController { } } - if isFirstLayout, let initialMonthIndex = initialMonthIndex, let frame = self.scrollLayout?.frames[initialMonthIndex] { + if let initialMonthIndex = initialMonthIndex, let frame = self.scrollLayout?.frames[initialMonthIndex] { var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0) if contentOffset < 0 { contentOffset = 0 @@ -821,23 +1179,216 @@ public final class CalendarMessageScreen: ViewController { if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height { contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height } + self.ignoreContentOffset = true self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) + self.ignoreContentOffset = false } + } else { + } updateMonthViews() } + private func selectionToolbarActionSelected() { + guard let selectionState = self.selectionState, let dayRange = selectionState.dayRange else { + return + } + var selectedCount = 0 + var minTimestamp: Int32? + var maxTimestamp: Int32? + for i in 0 ..< self.months.count { + let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) + + for day in 0 ..< self.months[i].numberOfDays { + let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) + let nextDayTimestamp = dayTimestamp + 24 * 60 * 60 + + let minDayTimestamp = dayTimestamp - 24 * 60 * 60 + let maxDayTimestamp = nextDayTimestamp - 24 * 60 * 60 + + if dayRange.contains(dayTimestamp) { + if let currentMinTimestamp = minTimestamp { + minTimestamp = min(minDayTimestamp, currentMinTimestamp) + } else { + minTimestamp = minDayTimestamp + } + if let currentMaxTimestamp = maxTimestamp { + maxTimestamp = max(maxDayTimestamp, currentMaxTimestamp) + } else { + maxTimestamp = maxDayTimestamp + } + selectedCount += 1 + } + } + } + + guard let minTimestampValue = minTimestamp, let maxTimestampValue = maxTimestamp else { + return + } + + if selectedCount == 0 { + return + } + + enum ClearType { + case savedMessages + case secretChat + case group + case channel + case user + } + + struct ClearInfo { + var canClearForMyself: ClearType? + var canClearForEveryone: ClearType? + var mainPeer: Peer + } + + let peerId = self.peerId + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + } else { + return + } + let _ = (self.context.account.postbox.transaction { transaction -> ClearInfo? in + guard let chatPeer = transaction.getPeer(peerId) else { + return nil + } + + let canClearForMyself: ClearType? + let canClearForEveryone: ClearType? + + if peerId == self.context.account.peerId { + canClearForMyself = .savedMessages + canClearForEveryone = nil + } else if chatPeer is TelegramSecretChat { + canClearForMyself = .secretChat + canClearForEveryone = nil + } else if let group = chatPeer as? TelegramGroup { + switch group.role { + case .creator: + canClearForMyself = .group + canClearForEveryone = nil + case .admin, .member: + canClearForMyself = .group + canClearForEveryone = nil + } + } else if let channel = chatPeer as? TelegramChannel { + if channel.hasPermission(.deleteAllMessages) { + if case .group = channel.info { + canClearForEveryone = .group + } else { + canClearForEveryone = .channel + } + } else { + canClearForEveryone = nil + } + canClearForMyself = nil + } else { + canClearForMyself = .user + + if let user = chatPeer as? TelegramUser, user.botInfo != nil { + canClearForEveryone = nil + } else { + canClearForEveryone = .user + } + } + + return ClearInfo( + canClearForMyself: canClearForMyself, + canClearForEveryone: canClearForEveryone, + mainPeer: chatPeer + ) + } + |> deliverOnMainQueue).start(next: { [weak self] info in + guard let strongSelf = self, let info = info else { + return + } + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + + let beginClear: (InteractiveHistoryClearingType) -> Void = { type in + guard let strongSelf = self else { + return + } + let _ = strongSelf.calendarSource.removeMessagesInRange(minTimestamp: minTimestampValue, maxTimestamp: maxTimestampValue, type: type, completion: { + }) + } + + if let _ = info.canClearForMyself ?? info.canClearForEveryone { + //TODO:localize + items.append(ActionSheetTextItem(title: "Are you sure you want to delete all messages for the \(selectedCount) selected days?")) + + if let canClearForEveryone = info.canClearForEveryone { + let text: String + let confirmationText: String + switch canClearForEveryone { + case .user: + text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(info.mainPeer).compactDisplayTitle).string + confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText + default: + text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone + confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText + } + let _ = confirmationText + items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + beginClear(.forEveryone) + + /*guard let strongSelf = self else { + return + } + + strongSelf.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: confirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { + beginClear(.forEveryone) + }) + ], parseMarkdown: true), in: .window(.root))*/ + })) + } + if let canClearForMyself = info.canClearForMyself { + let text: String + switch canClearForMyself { + case .savedMessages, .secretChat: + text = strongSelf.presentationData.strings.Conversation_DeleteManyMessages + default: + text = strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser + } + items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + beginClear(.forLocalPeer) + })) + } + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + strongSelf.controller?.present(actionSheet, in: .window(.root)) + }) + + self.controller?.toggleSelectPressed() + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.contextGestureContainerNode.cancelGesture() } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if let indicator = scrollView.value(forKey: "_verticalScrollIndicator") as? UIView { - indicator.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) - } + if !self.ignoreContentOffset { + if let indicator = scrollView.value(forKey: "_verticalScrollIndicator") as? UIView { + indicator.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) + } - self.updateMonthViews() + self.updateMonthViews() + } } func updateScrollLayoutIfNeeded() -> Bool { @@ -862,8 +1413,9 @@ public final class CalendarMessageScreen: ViewController { foregroundColor: .black, strings: self.presentationData.strings, theme: self.presentationData.theme, - navigateToDay: { _ in - } + dayAction: { _ in + }, + selectedDays: nil )), environment: { imageCache @@ -883,7 +1435,7 @@ public final class CalendarMessageScreen: ViewController { self.contextGestureContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight)) self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), 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: layout.size.width - 3.0 - 6.0) + self.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: max(layout.intrinsicInsets.bottom, self.scrollView.contentInset.top), left: 0.0, bottom: 0.0, right: layout.size.width - 3.0 - 6.0) return true } @@ -922,12 +1474,45 @@ public final class CalendarMessageScreen: ViewController { foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor, strings: self.presentationData.strings, theme: self.presentationData.theme, - navigateToDay: { [weak self] timestamp in + dayAction: { [weak self] timestamp in guard let strongSelf = self else { return } - strongSelf.navigateToDay(timestamp) - } + if var selectionState = strongSelf.selectionState { + if let dayRange = selectionState.dayRange { + if dayRange.lowerBound == dayRange.upperBound { + if timestamp < dayRange.lowerBound { + selectionState.dayRange = timestamp ... dayRange.upperBound + } else { + selectionState.dayRange = dayRange.lowerBound ... timestamp + } + } else { + selectionState.dayRange = timestamp ... timestamp + } + } else { + selectionState.dayRange = timestamp ... timestamp + } + strongSelf.selectionState = selectionState + + strongSelf.updateSelectionState() + } else { + outer: for month in strongSelf.months { + let firstDayTimestamp = Int32(month.firstDay.timeIntervalSince1970) + + for day in 0 ..< month.numberOfDays { + let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) + if dayTimestamp == timestamp { + if month.mediaByDay[day] != nil { + strongSelf.navigateToDay(timestamp) + } + + break outer + } + } + } + } + }, + selectedDays: self.selectionState?.dayRange )), environment: { self.imageCache @@ -949,6 +1534,38 @@ public final class CalendarMessageScreen: ViewController { } } + private func updateSelectionState() { + //TODO:localize + var title = "Calendar" + if let selectionState = self.selectionState, let dayRange = selectionState.dayRange { + var selectedCount = 0 + for i in 0 ..< self.months.count { + let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) + + for day in 0 ..< self.months[i].numberOfDays { + let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) + if dayRange.contains(dayTimestamp) { + selectedCount += 1 + } + } + } + + if selectedCount != 0 { + if selectedCount == 1 { + title = "1 day selected" + } else { + title = "\(selectedCount) days selected" + } + } + } + + self.controller?.navigationItem.title = title + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring)) + } + } + private func reloadMediaInfo() { guard let calendarState = self.calendarState else { return @@ -960,6 +1577,10 @@ public final class CalendarMessageScreen: ViewController { var updatedMedia: [Int: [Int: DayMedia]] = [:] for i in 0 ..< self.months.count { + if updatedMedia[i] == nil { + updatedMedia[i] = [:] + } + for day in 0 ..< self.months[i].numberOfDays { let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) @@ -971,9 +1592,6 @@ public final class CalendarMessageScreen: ViewController { mediaLoop: for media in message.media { switch media { case _ as TelegramMediaImage, _ as TelegramMediaFile: - if updatedMedia[i] == nil { - updatedMedia[i] = [:] - } updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media)) break mediaLoop default: @@ -1005,6 +1623,8 @@ public final class CalendarMessageScreen: ViewController { private let navigateToDay: (CalendarMessageScreen, Int32) -> Void private let previewDay: (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void + private var presentationData: PresentationData + public init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (CalendarMessageScreen, Int32) -> Void, previewDay: @escaping (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void) { self.context = context self.peerId = peerId @@ -1013,15 +1633,19 @@ public final class CalendarMessageScreen: ViewController { self.navigateToDay = navigateToDay self.previewDay = previewDay - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) self.navigationPresentation = .modal - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) //TODO:localize self.navigationItem.setTitle("Calendar", animated: false) + + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: false) + } } required public init(coder aDecoder: NSCoder) { @@ -1032,8 +1656,18 @@ public final class CalendarMessageScreen: ViewController { self.dismiss() } + @objc fileprivate func toggleSelectPressed() { + self.node.toggleSelectionMode() + + if self.node.selectionState != nil { + self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + } else { + self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + } + } + override public func loadDisplayNode() { - self.displayNode = Node(context: self.context, peerId: self.peerId, calendarSource: self.calendarSource, initialTimestamp: self.initialTimestamp, navigateToDay: { [weak self] timestamp in + self.displayNode = Node(controller: self, context: self.context, peerId: self.peerId, calendarSource: self.calendarSource, initialTimestamp: self.initialTimestamp, navigateToDay: { [weak self] timestamp in guard let strongSelf = self else { return } diff --git a/submodules/Display/Source/Toolbar.swift b/submodules/Display/Source/Toolbar.swift index 1d87078945..4d8ccc3930 100644 --- a/submodules/Display/Source/Toolbar.swift +++ b/submodules/Display/Source/Toolbar.swift @@ -2,12 +2,19 @@ import Foundation import UIKit public struct ToolbarAction: Equatable { + public enum Color: Equatable { + case accent + case custom(UIColor) + } + public let title: String public let isEnabled: Bool + public let color: Color - public init(title: String, isEnabled: Bool) { + public init(title: String, isEnabled: Bool, color: Color = .accent) { self.title = title self.isEnabled = isEnabled + self.color = color } } diff --git a/submodules/Display/Source/ToolbarNode.swift b/submodules/Display/Source/ToolbarNode.swift index 775fe381bd..d9ae99e453 100644 --- a/submodules/Display/Source/ToolbarNode.swift +++ b/submodules/Display/Source/ToolbarNode.swift @@ -117,8 +117,23 @@ public final class ToolbarNode: ASDisplayNode { self.rightTitle.attributedText = NSAttributedString(string: toolbar.rightAction?.title ?? "", font: Font.regular(17.0), textColor: (toolbar.rightAction?.isEnabled ?? false) ? self.theme.tabBarSelectedTextColor : self.theme.tabBarTextColor) self.rightButton.accessibilityLabel = toolbar.rightAction?.title - - self.middleTitle.attributedText = NSAttributedString(string: toolbar.middleAction?.title ?? "", font: Font.regular(17.0), textColor: (toolbar.middleAction?.isEnabled ?? false) ? self.theme.tabBarSelectedTextColor : self.theme.tabBarTextColor) + + let middleColor: UIColor + if let middleAction = toolbar.middleAction { + if middleAction.isEnabled { + switch middleAction.color { + case .accent: + middleColor = self.theme.tabBarSelectedTextColor + case let .custom(color): + middleColor = color + } + } else { + middleColor = self.theme.tabBarTextColor + } + } else { + middleColor = self.theme.tabBarTextColor + } + self.middleTitle.attributedText = NSAttributedString(string: toolbar.middleAction?.title ?? "", font: Font.regular(17.0), textColor: middleColor) self.middleButton.accessibilityLabel = toolbar.middleAction?.title var size = size diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 4ff15f44b9..80b0d3ec11 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -469,6 +469,14 @@ final class MessageHistoryTable: Table { self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, updatedMedia: &updatedMedia, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations, globalTagsOperations: &globalTagsOperations, pendingActionsOperations: &pendingActionsOperations, updatedMessageActionsSummaries: &updatedMessageActionsSummaries, updatedMessageTagSummaries: &updatedMessageTagSummaries, invalidateMessageTagSummaries: &invalidateMessageTagSummaries, localTagsOperations: &localTagsOperations, timestampBasedMessageAttributesOperations: ×tampBasedMessageAttributesOperations) } + + func clearHistoryInRange(peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { + var indices = self.allMessageIndices(peerId: peerId).filter { namespaces.contains($0.id.namespace) } + indices = indices.filter { index in + return index.timestamp >= minTimestamp && index.timestamp <= maxTimestamp + } + self.removeMessages(indices.map { $0.id }, operationsByPeerId: &operationsByPeerId, updatedMedia: &updatedMedia, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations, globalTagsOperations: &globalTagsOperations, pendingActionsOperations: &pendingActionsOperations, updatedMessageActionsSummaries: &updatedMessageActionsSummaries, updatedMessageTagSummaries: &updatedMessageTagSummaries, invalidateMessageTagSummaries: &invalidateMessageTagSummaries, localTagsOperations: &localTagsOperations, timestampBasedMessageAttributesOperations: ×tampBasedMessageAttributesOperations, forEachMedia: forEachMedia) + } func clearHistory(peerId: PeerId, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { let indices = self.allMessageIndices(peerId: peerId).filter { namespaces.contains($0.id.namespace) } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index de690c0ae2..ea1b24e9da 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -147,9 +147,9 @@ public final class Transaction { self.postbox?.withAllMessages(peerId: peerId, namespace: namespace, f) } - public func clearHistory(_ peerId: PeerId, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { + public func clearHistory(_ peerId: PeerId, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { assert(!self.disposed) - self.postbox?.clearHistory(peerId, namespaces: namespaces, forEachMedia: forEachMedia) + self.postbox?.clearHistory(peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: forEachMedia) } public func removeAllMessagesWithAuthor(_ peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace, forEachMedia: (Media) -> Void) { @@ -1806,10 +1806,14 @@ final class PostboxImpl { } } - fileprivate func clearHistory(_ peerId: PeerId, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { - self.messageHistoryTable.clearHistory(peerId: peerId, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) - for namespace in self.messageHistoryHoleIndexTable.existingNamespaces(peerId: peerId, holeSpace: .everywhere) where namespaces.contains(namespace) { - self.messageHistoryHoleIndexTable.remove(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... Int32.max - 1, operations: &self.currentPeerHoleOperations) + fileprivate func clearHistory(_ peerId: PeerId, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { + if let minTimestamp = minTimestamp, let maxTimestamp = maxTimestamp { + self.messageHistoryTable.clearHistoryInRange(peerId: peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) + } else { + self.messageHistoryTable.clearHistory(peerId: peerId, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) + for namespace in self.messageHistoryHoleIndexTable.existingNamespaces(peerId: peerId, holeSpace: .everywhere) where namespaces.contains(namespace) { + self.messageHistoryHoleIndexTable.remove(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... Int32.max - 1, operations: &self.currentPeerHoleOperations) + } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift index 2cc38783f0..c70a855765 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift @@ -4,10 +4,12 @@ import Postbox public final class AdMessageAttribute: MessageAttribute { public let opaqueId: Data public let startParam: String? + public let messageId: MessageId? - public init(opaqueId: Data, startParam: String?) { + public init(opaqueId: Data, startParam: String?, messageId: MessageId?) { self.opaqueId = opaqueId self.startParam = startParam + self.messageId = messageId } public init(decoder: PostboxDecoder) { diff --git a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift index 73639fcae5..790bb6d3f4 100644 --- a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift @@ -8,7 +8,7 @@ func cloudChatAddRemoveChatOperation(transaction: Transaction, peerId: PeerId, r transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveChatOperation(peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible, topMessageId: transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud))) } -func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, explicitTopMessageId: MessageId?, type: CloudChatClearHistoryType) { +func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, explicitTopMessageId: MessageId?, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { if type == .scheduledMessages { var messageIds: [MessageId] = [] transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.ScheduledCloud) { message -> Bool in @@ -24,7 +24,7 @@ func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) } if let topMessageId = topMessageId { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, type: type)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 1aedd3864c..8f61dfe59d 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -302,7 +302,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net } let deleteMessages: Signal if let inputPeer = apiInputPeer(peer), let topMessageId = operation.topMessageId ?? transaction.getTopPeerMessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud) { - deleteMessages = requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: topMessageId.id, justClear: false, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) + deleteMessages = requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: topMessageId.id, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) } else { deleteMessages = .complete() } @@ -326,7 +326,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net } else { reportSignal = .complete() } - return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) + return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .not(Namespaces.Message.allScheduled)) @@ -339,7 +339,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net } } -private func requestClearHistory(postbox: Postbox, network: Network, stateManager: AccountStateManager, inputPeer: Api.InputPeer, maxId: Int32, justClear: Bool, type: CloudChatClearHistoryType) -> Signal { +private func requestClearHistory(postbox: Postbox, network: Network, stateManager: AccountStateManager, inputPeer: Api.InputPeer, maxId: Int32, justClear: Bool, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) -> Signal { var flags: Int32 = 0 if justClear { flags |= 1 << 0 @@ -347,7 +347,16 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage if case .forEveryone = type { flags |= 1 << 1 } - let signal = network.request(Api.functions.messages.deleteHistory(flags: flags, peer: inputPeer, maxId: maxId, minDate: nil, maxDate: nil)) + var updatedMaxId = maxId + if minTimestamp != nil { + flags |= 1 << 2 + updatedMaxId = 0 + } + if maxTimestamp != nil { + flags |= 1 << 3 + updatedMaxId = 0 + } + let signal = network.request(Api.functions.messages.deleteHistory(flags: flags, peer: inputPeer, maxId: updatedMaxId, minDate: minTimestamp, maxDate: maxTimestamp)) |> map { result -> Api.messages.AffectedHistory? in return result } @@ -378,17 +387,21 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { - return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, type: operation.type) + return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) } else { return .complete() } } else if peer.id.namespace == Namespaces.Peer.CloudChannel, let inputChannel = apiInputChannel(peer) { - return network.request(Api.functions.channels.deleteHistory(channel: inputChannel, maxId: operation.topMessageId.id)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) - } - |> mapToSignal { _ -> Signal in + if operation.minTimestamp != nil { return .complete() + } else { + return network.request(Api.functions.channels.deleteHistory(channel: inputChannel, maxId: operation.topMessageId.id)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } } } else { assertionFailure() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift index 610bd6de7b..79e7c58533 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift @@ -112,17 +112,23 @@ public extension CloudChatClearHistoryType { public final class CloudChatClearHistoryOperation: PostboxCoding { public let peerId: PeerId public let topMessageId: MessageId + public let minTimestamp: Int32? + public let maxTimestamp: Int32? public let type: CloudChatClearHistoryType - public init(peerId: PeerId, topMessageId: MessageId, type: CloudChatClearHistoryType) { + public init(peerId: PeerId, topMessageId: MessageId, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { self.peerId = peerId self.topMessageId = topMessageId + self.minTimestamp = minTimestamp + self.maxTimestamp = maxTimestamp self.type = type } public init(decoder: PostboxDecoder) { self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0)) self.topMessageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)) + self.minTimestamp = decoder.decodeOptionalInt32ForKey("minTimestamp") + self.maxTimestamp = decoder.decodeOptionalInt32ForKey("maxTimestamp") self.type = CloudChatClearHistoryType(rawValue: decoder.decodeInt32ForKey("type", orElse: 0)) ?? .forLocalPeer } @@ -131,6 +137,16 @@ public final class CloudChatClearHistoryOperation: PostboxCoding { encoder.encodeInt64(self.topMessageId.peerId.toInt64(), forKey: "m.p") encoder.encodeInt32(self.topMessageId.namespace, forKey: "m.n") encoder.encodeInt32(self.topMessageId.id, forKey: "m.i") + if let minTimestamp = self.minTimestamp { + encoder.encodeInt32(minTimestamp, forKey: "minTimestamp") + } else { + encoder.encodeNil(forKey: "minTimestamp") + } + if let maxTimestamp = self.maxTimestamp { + encoder.encodeInt32(maxTimestamp, forKey: "maxTimestamp") + } else { + encoder.encodeNil(forKey: "maxTimestamp") + } encoder.encodeInt32(self.type.rawValue, forKey: "type") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 1671d33c1d..037d7abd0a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -111,7 +111,7 @@ private class AdMessagesHistoryContextImpl { func toMessage(peerId: PeerId, transaction: Transaction) -> Message { var attributes: [MessageAttribute] = [] - attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, startParam: self.startParam)) + attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, startParam: self.startParam, messageId: self.messageId)) if !self.textEntities.isEmpty { let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) attributes.append(attribute) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 7aacae2e6a..6f256e3306 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -87,7 +87,24 @@ func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } } - transaction.clearHistory(peerId, namespaces: namespaces, forEachMedia: { _ in + transaction.clearHistory(peerId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in + }) +} + +func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) { + if peerId.namespace == Namespaces.Peer.SecretChat { + var resourceIds: [MediaResourceId] = [] + transaction.withAllMessages(peerId: peerId, { message in + if message.timestamp >= minTimestamp && message.timestamp <= maxTimestamp { + addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds) + } + return true + }) + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + } + } + transaction.clearHistory(peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 0cf5dbbdef..4a3e8da4c8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -96,10 +96,43 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } +func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType) -> Signal { + return postbox.transaction { transaction -> Void in + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type)) + if type == .scheduledMessages { + } else { + _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allScheduled)) + } + } else if peerId.namespace == Namespaces.Peer.SecretChat { + /*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) + + if let state = transaction.getPeerChatState(peerId) as? SecretChatState { + var layer: SecretChatLayer? + switch state.embeddedState { + case .terminated, .handshake: + break + case .basicLayer: + layer = .layer8 + case let .sequenceBasedLayer(sequenceState): + layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer + } + + if let layer = layer { + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state) + if updatedState != state { + transaction.setPeerChatState(peerId, state: updatedState) + } + } + }*/ + } + } +} + func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { return postbox.transaction { transaction -> Void in if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { - cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, type: CloudChatClearHistoryType(type)) + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .just(Namespaces.Message.allScheduled)) } else { @@ -110,7 +143,7 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .not(Namespaces.Message.allScheduled)) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { - cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), type: CloudChatClearHistoryType(type)) + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, namespaces: .all) } if let topIndex = topIndex { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 4ac4b36196..5a06c2ea76 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -581,6 +581,24 @@ public final class SparseMessageCalendar { self.loadMore() } + func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable { + var removeKeys: [Int32] = [] + for (id, message) in self.state.messagesByDay { + if message.timestamp >= minTimestamp && message.timestamp <= maxTimestamp { + removeKeys.append(id) + } + } + for id in removeKeys { + self.state.messagesByDay.removeValue(forKey: id) + } + + self.statePromise.set(.single(self.state)) + + return _internal_clearHistoryInRangeInteractively(postbox: self.account.postbox, peerId: self.peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type).start(completed: { + completion() + }) + } + private func loadMore() { guard let nextRequestOffset = self.state.nextRequestOffset else { return @@ -754,4 +772,14 @@ public final class SparseMessageCalendar { impl.maybeLoadMore() } } + + public func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable { + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.removeMessagesInRange(minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type, completion: completion)) + } + + return disposable + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index d4dd82733a..da47eac52d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -68,7 +68,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let bot = author as? TelegramUser, bot.botInfo != nil, let startParam = adAttribute.startParam { navigationData = .withBotStartPayload(ChatControllerInitialBotStart(payload: startParam, behavior: .interactive)) } else { - navigationData = .chat(textInputState: nil, subject: nil, peekData: nil) + var subject: ChatControllerSubject? + if let messageId = adAttribute.messageId { + subject = .message(id: messageId, highlight: true, timecode: nil) + } + navigationData = .chat(textInputState: nil, subject: subject, peekData: nil) } item.controllerInteraction.openPeer(author.id, navigationData, nil) } else { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 5447b9aa0b..db10757c44 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -1172,6 +1172,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } } if let loadSignal = result.loadSignal { + layer.disposable?.dispose() layer.disposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self, weak layer, weak displayItem] image in guard let layer = layer else { @@ -2167,9 +2168,6 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro func currentTopTimestamp() -> Int32? { var timestamp: Int32? self.itemGrid.forEachVisibleItem { item in - if timestamp != nil { - return - } guard let itemLayer = item.layer as? ItemLayer else { return }