diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index 261ef74d6a..de5c9ddf17 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -20,11 +20,13 @@ public struct MessageHistoryScrollToSubject: Equatable { public var index: MessageHistoryAnchorIndex public var quote: Quote? + public var todoTaskId: Int32? public var setupReply: Bool - public init(index: MessageHistoryAnchorIndex, quote: Quote?, setupReply: Bool = false) { + public init(index: MessageHistoryAnchorIndex, quote: Quote?, todoTaskId: Int32? = nil, setupReply: Bool = false) { self.index = index self.quote = quote + self.todoTaskId = todoTaskId self.setupReply = setupReply } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index fc9bf4e43b..14aaf4c1f1 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -837,7 +837,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: size.width - statusSize.width - 15.0, y: floorToScreenPixels((size.height - statusSize.height) / 2.0)), size: statusSize)) self.statusNode.foregroundNodeColor = state.textColor - self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0)) : .none) + self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0), animateRotation: true) : .none) } } diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 637a97fc28..97a0506535 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -530,6 +530,8 @@ final class ComposePollScreenComponent: Component { let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment + let theme = environment.theme.withModalBlocksBackground() + if self.component == nil { self.isQuiz = component.isQuiz ?? false @@ -682,7 +684,7 @@ final class ComposePollScreenComponent: Component { let sectionSpacing: CGFloat = 24.0 if themeUpdated { - self.backgroundColor = environment.theme.list.blocksBackgroundColor + self.backgroundColor = theme.list.blocksBackgroundColor } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -695,7 +697,7 @@ final class ComposePollScreenComponent: Component { pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.pollTextInputState, context: component.context, - theme: environment.theme, + theme: theme, strings: environment.strings, resetText: self.resetPollText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) @@ -737,12 +739,12 @@ final class ComposePollScreenComponent: Component { let pollTextSectionSize = self.pollTextSection.update( transition: transition, component: AnyComponent(ListSectionComponent( - theme: environment.theme, + theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_TextHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -790,7 +792,7 @@ final class ComposePollScreenComponent: Component { pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent( externalState: pollOption.textInputState, context: component.context, - theme: environment.theme, + theme: theme, strings: environment.strings, resetText: pollOption.resetText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) @@ -916,7 +918,7 @@ final class ComposePollScreenComponent: Component { let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update( configuration: ListSectionContentView.Configuration( - theme: environment.theme, + theme: theme, displaySeparators: true, extendsItemHighlightToSection: false, background: .all @@ -934,7 +936,7 @@ final class ComposePollScreenComponent: Component { text: .plain(NSAttributedString( string: environment.strings.CreatePoll_OptionsHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -983,7 +985,7 @@ final class ComposePollScreenComponent: Component { if pollOptionsLimitReached { pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none) pollOptionsComponent = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor)), + text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor)), maximumNumberOfLines: 0 )) } else { @@ -1015,7 +1017,7 @@ final class ComposePollScreenComponent: Component { pollOptionsComponent = AnyComponent(AnimatedTextComponent( font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - color: environment.theme.list.freeTextColor, + color: theme.list.freeTextColor, items: pollOptionsFooterItems )) } @@ -1055,13 +1057,13 @@ final class ComposePollScreenComponent: Component { var pollSettingsSectionItems: [AnyComponentWithIdentity] = [] if canBePublic { pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent( - theme: environment.theme, + theme: theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_Anonymous, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), @@ -1077,13 +1079,13 @@ final class ComposePollScreenComponent: Component { )))) } pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent( - theme: environment.theme, + theme: theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_MultipleChoice, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), @@ -1101,13 +1103,13 @@ final class ComposePollScreenComponent: Component { action: nil )))) pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent( - theme: environment.theme, + theme: theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_Quiz, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), @@ -1128,13 +1130,13 @@ final class ComposePollScreenComponent: Component { let pollSettingsSectionSize = self.pollSettingsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( - theme: environment.theme, + theme: theme, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_QuizInfo, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -1158,12 +1160,12 @@ final class ComposePollScreenComponent: Component { let quizAnswerSectionSize = self.quizAnswerSection.update( transition: transition, component: AnyComponent(ListSectionComponent( - theme: environment.theme, + theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_ExplanationHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -1171,7 +1173,7 @@ final class ComposePollScreenComponent: Component { text: .plain(NSAttributedString( string: environment.strings.CreatePoll_ExplanationInfo, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -1179,7 +1181,7 @@ final class ComposePollScreenComponent: Component { AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.quizAnswerTextInputState, context: component.context, - theme: environment.theme, + theme: theme, strings: environment.strings, resetText: self.resetQuizAnswerText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) @@ -1326,7 +1328,7 @@ final class ComposePollScreenComponent: Component { component: AnyComponent(EmojiSuggestionsComponent( context: component.context, userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), + theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor), animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, files: value, diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 8e1eb1f63d..814b1d9c52 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -66,6 +66,9 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/ToastComponent", "//submodules/SemanticStatusNode", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/AvatarNode", + "//submodules/PhotoResources", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/AdRemainingProgressComponent.swift b/submodules/GalleryUI/Sources/Items/AdRemainingProgressComponent.swift new file mode 100644 index 0000000000..00afa10f2b --- /dev/null +++ b/submodules/GalleryUI/Sources/Items/AdRemainingProgressComponent.swift @@ -0,0 +1,212 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import ComponentFlow +import SemanticStatusNode +import AnimatedTextComponent + +public final class AdRemainingProgressComponent: Component { + public let initialTimestamp: Int32 + public let minDisplayDuration: Int32 + public let maxDisplayDuration: Int32 + public let action: (Bool) -> Void + + public init( + initialTimestamp: Int32, + minDisplayDuration: Int32, + maxDisplayDuration: Int32, + action: @escaping (Bool) -> Void + ) { + self.initialTimestamp = initialTimestamp + self.minDisplayDuration = minDisplayDuration + self.maxDisplayDuration = maxDisplayDuration + self.action = action + } + + public static func ==(lhs: AdRemainingProgressComponent, rhs: AdRemainingProgressComponent) -> Bool { + if lhs.initialTimestamp != rhs.initialTimestamp { + return false + } + if lhs.minDisplayDuration != rhs.minDisplayDuration { + return false + } + if lhs.maxDisplayDuration != rhs.maxDisplayDuration { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var component: AdRemainingProgressComponent? + private weak var componentState: EmptyComponentState? + + private let node: SemanticStatusNode + private var textComponent = ComponentView() + private var cancelIcon = UIImageView() + + private var progress: Double = 1.0 + + override init(frame: CGRect) { + self.node = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) + self.node.isUserInteractionEnabled = false + + self.cancelIcon.alpha = 0.0 + self.cancelIcon.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.node.view) + self.addSubview(self.cancelIcon) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.layer.removeAnimation(forKey: "opacity") + self.alpha = 0.7 + } else { + self.alpha = 1.0 + self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2) + } + } + } + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(self.progress < .ulpOfOne) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + var timer: SwiftSignalKit.Timer? + + func update(component: AdRemainingProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + self.timer = SwiftSignalKit.Timer(timeout: 0.25, repeat: true, completion: { [weak self] progress in + guard let self else { + return + } + self.componentState?.updated(transition: .easeInOut(duration: 0.2)) + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + self.component = component + self.componentState = state + + let size = CGSize(width: 24.0, height: 24.0) + let color = UIColor(rgb: 0x64d2ff) + + if self.cancelIcon.image == nil { + self.cancelIcon.image = generateCancelIcon(color: color) + self.node.foregroundNodeColor = color + } + + var progress = 0.0 + let currentTimestamp = CFAbsoluteTimeGetCurrent() + let minTimestamp = Double(component.initialTimestamp + component.minDisplayDuration) + let initialTimestamp = Double(component.initialTimestamp) + + let remaining = min(9, max(1, minTimestamp - currentTimestamp)) + + var textIsHidden = false + if currentTimestamp >= initialTimestamp && currentTimestamp <= minTimestamp { + progress = (minTimestamp - currentTimestamp) / (minTimestamp - initialTimestamp) + } else { + progress = 0 + textIsHidden = true + } + self.progress = progress + + let textSize = self.textComponent.update( + transition: transition, + component: AnyComponent( + AnimatedTextComponent( + font: Font.regular(14.0), + color: color, + items: [AnimatedTextComponent.Item(id: 0, content: .number(Int(remaining), minDigits: 1))] + ) + ), + environment: {}, + containerSize: size + ) + + let iconTransition = ComponentTransition(animation: .curve(duration: 0.25, curve: .spring)) + if let textView = self.textComponent.view { + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.addSubview(textView) + } + textView.frame = CGRect(origin: CGPoint(x: (size.width - textSize.width) / 2.0, y: (size.height - textSize.height) / 2.0), size: textSize) + iconTransition.setAlpha(view: textView, alpha: textIsHidden ? 0.0 : 1.0) + iconTransition.setAlpha(view: self.cancelIcon, alpha: textIsHidden ? 1.0 : 0.0) + } + + if let icon = self.cancelIcon.image { + self.cancelIcon.bounds = CGRect(origin: .zero, size: icon.size) + + var iconScale = 0.7 + var iconAlpha = 1.0 + var iconPosition = CGPoint(x: size.width / 2.0 + 10.0, y: size.height / 2.0 - 10.0) + if progress > 0.8 { + iconScale = 0.01 + iconAlpha = 0.0 + } else if progress < .ulpOfOne { + iconScale = 1.0 + iconPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } + iconTransition.setAlpha(view: self.cancelIcon, alpha: iconAlpha) + iconTransition.setScale(view: self.cancelIcon, scale: iconScale) + iconTransition.setPosition(view: self.cancelIcon, position: iconPosition) + } + + self.node.frame = CGRect(origin: .zero, size: size) + self.node.transitionToState(.progress(value: max(0.0, min(1.0, progress)), cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 1.0 + UIScreenPixel), animateRotation: false), updateCutout: false) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateCancelIcon(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setStrokeColor(color.cgColor) + + context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + + context.strokePath() + + context.move(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + + context.strokePath() + }) +} + diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 9d5fe45f50..aae9228533 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -756,7 +756,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index bdd419e941..58f0aca788 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -223,85 +223,190 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode } } -private let fullscreenImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white) -private let minimizeImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Minimize"), color: .white) - private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode { - private let wrapperNode: ASDisplayNode - private let fullscreenNode: HighlightableButtonNode - private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)? - - var action: ((Bool) -> Void)? - - override init() { - self.wrapperNode = ASDisplayNode() - self.wrapperNode.alpha = 0.0 + private var context: AccountContext? - self.fullscreenNode = HighlightableButtonNode() - self.fullscreenNode.setImage(fullscreenImage, for: .normal) - self.fullscreenNode.setImage(minimizeImage, for: .selected) - self.fullscreenNode.setImage(minimizeImage, for: [.selected, .highlighted]) + private var adView = ComponentView() - super.init() + private var message: Message? + private var adContext: AdMessagesHistoryContext? + private var adState: (startDelay: Int32?, betweenDelay: Int32?, messages: [Message])? + private let adDisposable = MetaDisposable() + + private var program: [(Int32, Message?)] = [] + + var performAction: ((GalleryControllerInteractionTapAction) -> Void)? + var presentPremiumDemo: (() -> Void)? + var openMoreMenu: ((ContextReferenceContentNode, Message) -> Void)? + + private var validLayout: (size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets)? - self.addSubnode(self.wrapperNode) - self.wrapperNode.addSubnode(self.fullscreenNode) - - self.fullscreenNode.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside) + deinit { + self.adDisposable.dispose() } + func setMessage(context: AccountContext, message: Message) { + self.context = context + guard self.message?.id != message.id else { + return + } + self.message = message + + let adContext = context.engine.messages.adMessages(peerId: message.id.peerId, messageId: message.id) + self.adContext = adContext + self.adDisposable.set((adContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + if !state.messages.isEmpty { + self.adState = (state.startDelay, state.betweenDelay, state.messages) + + var startTime = Int32(CFAbsoluteTimeGetCurrent()) // + (state.startDelay ?? 0) + var program: [(Int32, Message?)] = [] + var maxDisplayDuration: Int32 = 30 + for message in state.messages { + if !program.isEmpty { + program.append((startTime, nil)) + startTime += (state.betweenDelay ?? 0) + } + program.append((startTime, message)) + + if let adAttribute = message.adAttribute { + maxDisplayDuration = adAttribute.maxDisplayDuration ?? 30 + startTime += maxDisplayDuration + } + } + program.append((startTime + maxDisplayDuration, nil)) + self.program = program + } else { + self.adState = nil + + self.program = [] + } + + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + } + })) + } + + var timer: SwiftSignalKit.Timer? + var hiddenMessages = Set() + var isAnimatingOut = false + var reportedMessages = Set() + override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, metrics, insets) - let isLandscape = size.width > size.height - self.fullscreenNode.isSelected = isLandscape + if self.timer == nil { + self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] progress in + guard let self else { + return + } + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } - let iconSize: CGFloat = 42.0 - let inset: CGFloat = 4.0 - let buttonFrame = CGRect(origin: CGPoint(x: size.width - iconSize - inset - insets.right, y: size.height - iconSize - inset - insets.bottom), size: CGSize(width: iconSize, height: iconSize)) - transition.updateFrame(node: self.wrapperNode, frame: buttonFrame) - transition.updateFrame(node: self.fullscreenNode, frame: CGRect(origin: CGPoint(), size: buttonFrame.size)) + let isLandscape = size.width > size.height + let _ = isLandscape + + let currentTime = Int32(CFAbsoluteTimeGetCurrent()) + var currentAd: (Int32, Message?)? + + for (time, maybeMessage) in program { + if currentTime > time { + currentAd = (time, maybeMessage) + } + } + + if let context = self.context, let (initialTimestamp, maybeMessage) = currentAd, let adMessage = maybeMessage, !self.hiddenMessages.contains(adMessage.id) { + if let adAttribute = adMessage.adAttribute { + if !self.reportedMessages.contains(adAttribute.opaqueId) { + self.reportedMessages.insert(adAttribute.opaqueId) + context.engine.messages.markAdAsSeen(opaqueId: adAttribute.opaqueId) + } + } + + let sideInset: CGFloat = 16.0 + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let adSize = self.adView.update( + transition: .immediate, + component: AnyComponent( + VideoAdComponent( + context: context, + theme: presentationData.theme, + strings: presentationData.strings, + message: EngineMessage(adMessage), + initialTimestamp: initialTimestamp, + action: { [weak self] available in + guard let self else { + return + } + if available { + self.hiddenMessages.insert(adMessage.id) + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + } + } else { + self.presentPremiumDemo?() + } + }, + adAction: { [weak self] in + if let self, let ad = adMessage.adAttribute { + context.engine.messages.markAdAction(opaqueId: ad.opaqueId, media: false, fullscreen: false) + self.performAction?(.url(url: ad.url, concealed: false)) + } + }, + moreAction: { [weak self] sourceNode in + if let self { + self.openMoreMenu?(sourceNode, adMessage) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: size.width - sideInset * 2.0, height: 200.0) + ) + if let adView = self.adView.view { + if adView.superview == nil { + self.view.addSubview(adView) + + adView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + adView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + transition.updateFrame(view: adView, frame: CGRect(origin: CGPoint(x: floor((size.width - adSize.width) / 2.0), y: size.height - adSize.height - insets.bottom), size: adSize)) + } + } else if let adView = self.adView.view, !self.isAnimatingOut { + self.isAnimatingOut = true + adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + adView.removeFromSuperview() + Queue.mainQueue().after(0.1) { + adView.layer.removeAllAnimations() + } + self.isAnimatingOut = false + }) + } } override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { - if !self.visibilityAlpha.isZero { - transition.updateAlpha(node: self.wrapperNode, alpha: 1.0) - } + } override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - transition.updateAlpha(node: self.wrapperNode, alpha: 0.0) - } - - override func setVisibilityAlpha(_ alpha: CGFloat) { - super.setVisibilityAlpha(alpha) - self.updateFullscreenButtonVisibility() - } - - func updateFullscreenButtonVisibility() { - self.wrapperNode.alpha = self.visibilityAlpha - if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.0, metrics: validLayout.1, insets: validLayout.2, isHidden: false, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - - @objc func toggleFullscreenPressed() { - var toLandscape = false - if let (size, _, _) = self.validLayout, size.width < size.height { - toLandscape = true - } - if toLandscape { - self.wrapperNode.alpha = 0.0 - } - self.action?(toLandscape) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.wrapperNode.frame.contains(point) { - return nil + if let adView = self.adView.view, adView.frame.contains(point) { + return super.hitTest(point, with: event) } - return super.hitTest(point, with: event) + return nil } } @@ -1149,15 +1254,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) } - - self.overlayContentNode.action = { [weak self] toLandscape in - guard let self else { - return - } - self.updateControlsVisibility(!toLandscape) - self.updateOrientation(toLandscape ? .landscapeRight : .portrait) - } - + self.statusButtonNode.addSubnode(self.statusNode) self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) @@ -1262,7 +1359,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let self else { return } - self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false) + var adMessage: Message? + if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { + adMessage = message + } + self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, adMessage: adMessage, isSettings: false) } self.titleContentView = GalleryTitleView(frame: CGRect()) @@ -1394,7 +1495,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.dismiss() } } - + func setupItem(_ item: UniversalVideoGalleryItem) { if self.item?.content.id != item.content.id { var chapters = parseMediaPlayerChapters(item.caption) @@ -1523,14 +1624,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } let _ = isAdaptive - - let dimensions = item.content.dimensions - if dimensions.height > 0.0 { - if dimensions.width / dimensions.height < 1.33 || isAnimated { - self.overlayContentNode.isHidden = true - } - } - + if let videoNode = self.videoNode { videoNode.canAttachContent = false videoNode.removeFromSupernode() @@ -1993,6 +2087,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd) + + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { + self.overlayContentNode.performAction = item.performAction + self.overlayContentNode.presentPremiumDemo = { [weak self] in + self?.presentPremiumDemo() + } + self.overlayContentNode.openMoreMenu = { [weak self] sourceNode, adMessage in + self?.openMoreMenu(sourceNode: sourceNode, gesture: nil, adMessage: adMessage, isSettings: false, actionsOnTop: true) + } + self.overlayContentNode.setMessage(context: item.context, message: message) + } } override func controlsVisibilityUpdated(isVisible: Bool) { @@ -3109,15 +3214,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) } - private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) { + private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, adMessage: Message?, isSettings: Bool, actionsOnTop: Bool = false) { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } var dismissImpl: (() -> Void)? let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> - if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { - items = self.adMenuMainItems() |> map { items in + if let adMessage { + items = self.adMenuMainItems(message: adMessage) |> map { items in return (items, []) } } else { @@ -3126,7 +3231,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode, actionsOnTop: actionsOnTop)), items: items |> map { items in if !items.topItems.isEmpty { return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems)) } else { @@ -3164,8 +3269,22 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return speedList } - private func adMenuMainItems() -> Signal<[ContextMenuItem], NoError> { - guard case let .message(message, _) = self.item?.contentInfo, let adAttribute = message.adAttribute else { + private func presentPremiumDemo() { + var replaceImpl: ((ViewController) -> Void)? + let controller = self.context.sharedContext.makePremiumDemoController(context: self.context, subject: .noAds, forceDark: true, action: { + let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: true, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self.baseNavigationController() { + navigationController.pushViewController(controller) + } + } + + private func adMenuMainItems(message: Message) -> Signal<[ContextMenuItem], NoError> { + guard let adAttribute = message.adAttribute else { return .single([]) } @@ -3244,18 +3363,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { [weak self] c, _ in - c?.dismiss(completion: { - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - if let navigationController = self?.baseNavigationController() as? NavigationController { - navigationController.pushViewController(controller) - } + c?.dismiss(completion: { [weak self] in + self?.presentPremiumDemo() }) }))) } @@ -3765,7 +3874,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } @objc private func settingsButtonPressed() { - self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true) + self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, adMessage: nil, isSettings: true) } override func adjustForPreviewing() { @@ -3775,7 +3884,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, nil)) + return .single((self.footerContentNode, self.overlayContentNode)) } func updatePlaybackRate(_ playbackRate: Double?) { @@ -3940,14 +4049,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ContextReferenceContentNode - - init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + private let actionsOnTop: Bool + + init(controller: ViewController, sourceNode: ContextReferenceContentNode, actionsOnTop: Bool) { self.controller = controller self.sourceNode = sourceNode + self.actionsOnTop = actionsOnTop } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom) } } diff --git a/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift b/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift new file mode 100644 index 0000000000..2f23afe3a1 --- /dev/null +++ b/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift @@ -0,0 +1,289 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import ComponentFlow +import MultilineTextComponent +import Postbox +import TelegramCore +import TelegramPresentationData +import ContextUI +import PlainButtonComponent +import AvatarNode +import AccountContext +import PhotoResources + +final class VideoAdComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let message: EngineMessage + let initialTimestamp: Int32 + let action: (Bool) -> Void + let adAction: () -> Void + let moreAction: (ContextReferenceContentNode) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + message: EngineMessage, + initialTimestamp: Int32, + action: @escaping (Bool) -> Void, + adAction: @escaping () -> Void, + moreAction: @escaping (ContextReferenceContentNode) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.message = message + self.initialTimestamp = initialTimestamp + self.action = action + self.adAction = adAction + self.moreAction = moreAction + } + + static func ==(lhs: VideoAdComponent, rhs: VideoAdComponent) -> Bool { + if lhs.message != rhs.message { + return false + } + if lhs.initialTimestamp != rhs.initialTimestamp { + return false + } + return true + } + + final class View: UIView { + private var component: VideoAdComponent? + private weak var componentState: EmptyComponentState? + + private let wrapperView: UIView + private let backgroundView: UIView + private let imageNode: TransformImageNode + private let title = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + private let buttonNode: ContextReferenceContentNode + private let progress = ComponentView() + + private var adIcon: UIImage? + + override init(frame: CGRect) { + self.wrapperView = UIView() + self.wrapperView.clipsToBounds = true + self.wrapperView.layer.cornerRadius = 14.0 + if #available(iOS 13.0, *) { + self.wrapperView.layer.cornerCurve = .continuous + } + + self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + + self.imageNode = TransformImageNode() + self.imageNode.isUserInteractionEnabled = false + + self.buttonNode = ContextReferenceContentNode() + + super.init(frame: frame) + + self.addSubview(self.wrapperView) + self.wrapperView.addSubview(self.backgroundView) + self.wrapperView.addSubview(self.buttonNode.view) + self.wrapperView.addSubview(self.imageNode.view) + + self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + @objc private func tapped() { + if let component = self.component { + component.adAction() + } + } + + func update(component: VideoAdComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isFirstTime = self.component == nil + self.component = component + + let titleString = component.message.author?.compactDisplayTitle ?? "" + + var media: Media? + if let photo = component.message.media.first as? TelegramMediaImage { + media = photo + } else if let file = component.message.media.first as? TelegramMediaFile { + media = file + } + if isFirstTime { + let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> + if let photo = media as? TelegramMediaImage { + signal = mediaGridMessagePhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: photo)) + } else if let file = media as? TelegramMediaFile { + signal = mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file)) + } else { + signal = .complete() + } + self.imageNode.setSignal(signal) + } + + let leftInset: CGFloat = media != nil ? 51.0 : 16.0 + let rightInset: CGFloat = 60.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(14.0), textColor: .white))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height) + ) + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.message.text, font: Font.regular(14.0), textColor: .white)), + maximumNumberOfLines: 2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height) + ) + + let contentHeight = titleSize.height + 3.0 + textSize.height + + let size = CGSize(width: availableSize.width, height: contentHeight + 24.0) + + let imageSize = CGSize(width: 30.0, height: 30.0) + self.imageNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) + + let makeLayout = self.imageNode.asyncLayout() + let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: .zero)) + apply() + + let contentOriginY = floor((size.height - contentHeight) / 2.0) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.wrapperView.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY + contentHeight - textSize.height), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.wrapperView.addSubview(textView) + } + textView.frame = textFrame + } + + let color = UIColor(rgb: 0x64d2ff) + if self.adIcon == nil { + self.adIcon = generateAdIcon(color: color, strings: component.strings) + } + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + Image(image: self.adIcon, contentMode: .center) + ), + effectAlignment: .center, + action: { [weak self] in + if let self { + component.moreAction(self.buttonNode) + } + }, + animateScale: false + ) + ), + environment: {}, + containerSize: availableSize + ) + let buttonFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor(titleFrame.midY - buttonSize.height / 2.0) + 1.0), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.wrapperView.addSubview(buttonView) + } + buttonView.frame = buttonFrame + } + self.buttonNode.frame = buttonFrame + + let progressSize = self.progress.update( + transition: .immediate, + component: AnyComponent( + AdRemainingProgressComponent( + initialTimestamp: component.initialTimestamp, + minDisplayDuration: 10, + maxDisplayDuration: 30, + action: { [weak self] available in + guard let self, let component = self.component else { + return + } + component.action(available) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + + let progressFrame = CGRect(origin: CGPoint(x: size.width - progressSize.width - 16.0, y: floor((size.height - progressSize.height) / 2.0)), size: progressSize) + if let progressView = self.progress.view { + if progressView.superview == nil { + self.wrapperView.addSubview(progressView) + } + progressView.frame = progressFrame + } + + self.wrapperView.frame = CGRect(origin: .zero, size: size) + self.backgroundView.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateAdIcon(color: UIColor, strings: PresentationStrings) -> UIImage? { + let titleString = NSAttributedString(string: strings.ChatList_Search_Ad, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center) + let stringRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil) + + return generateImage(CGSize(width: floor(stringRect.width) + 18.0, height: 15.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(color.withMultipliedAlpha(0.1).cgColor) + context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: size.height / 2.0).cgPath) + context.fillPath() + + context.setFillColor(color.cgColor) + + let circleSize = CGSize(width: 2.0 - UIScreenPixel, height: 2.0 - UIScreenPixel) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 3.0 + UIScreenPixel), size: circleSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 7.0 - UIScreenPixel), size: circleSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 10.0), size: circleSize)) + + let textRect = CGRect( + x: 5.0, + y: (size.height - stringRect.height) / 2.0 - UIScreenPixel, + width: stringRect.width, + height: stringRect.height + ) + + UIGraphicsPushContext(context) + titleString.draw(in: textRect) + UIGraphicsPopContext() + }) +} diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 37b56715f8..7b4fe7a8b6 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -1360,7 +1360,7 @@ public final class ListMessageFileItemNode: ListMessageNode { switch fetchStatus { case let .Fetching(_, progress): if item.isDownloadList { - iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil) + iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil, animateRotation: true) } case .Local: if isAudio || isInstantVideo { diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift index 85e18dd8fc..9cb283d27b 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift @@ -32,7 +32,7 @@ public enum SemanticStatusNodeState: Equatable { case play case pause case check(appearance: CheckAppearance?) - case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?) + case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?, animateRotation: Bool) case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?) case customIcon(UIImage) } @@ -136,12 +136,12 @@ private extension SemanticStatusNodeState { } else { return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance) } - case let .progress(value, cancelEnabled, appearance): + case let .progress(value, cancelEnabled, appearance, animateRotation): if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled { current.updateValue(value: value) return current } else { - return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance) + return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance, animateRotation: animateRotation) } } } diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift index 671c33180f..0c2c8162a4 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift @@ -25,13 +25,15 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { let value: CGFloat? let displayCancel: Bool let appearance: SemanticStatusNodeState.ProgressAppearance? + let animateRotation: Bool let timestamp: Double - init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) { + init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, animateRotation: Bool, timestamp: Double) { self.transitionFraction = transitionFraction self.value = value self.displayCancel = displayCancel self.appearance = appearance + self.animateRotation = animateRotation self.timestamp = timestamp super.init() @@ -59,6 +61,10 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { var endAngle: CGFloat if let value = self.value { progress = value + if !self.animateRotation { + progress = 1.0 - progress + } + startAngle = -CGFloat.pi / 2.0 endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle @@ -98,14 +104,16 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { pathDiameter = diameter - lineWidth - 2.5 * 2.0 } - var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) - angle *= 4.0 + if self.animateRotation { + var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) + angle *= 4.0 + + context.translateBy(x: diameter / 2.0, y: diameter / 2.0) + context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) + context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) + } - context.translateBy(x: diameter / 2.0, y: diameter / 2.0) - context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) - context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) - - let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: self.animateRotation) path.lineWidth = lineWidth path.lineCapStyle = .round path.stroke() @@ -147,6 +155,7 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { var value: CGFloat? let displayCancel: Bool let appearance: SemanticStatusNodeState.ProgressAppearance? + let animateRotation: Bool var transition: SemanticStatusNodeProgressTransition? var isAnimating: Bool { @@ -155,10 +164,11 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { var requestUpdate: () -> Void = {} - init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) { + init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, animateRotation: Bool) { self.value = value self.displayCancel = displayCancel self.appearance = appearance + self.animateRotation = animateRotation } func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { @@ -178,7 +188,7 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { } else { resolvedValue = nil } - return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp) + return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, animateRotation: self.animateRotation, timestamp: timestamp) } func maskView() -> UIView? { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift index eb83d75458..f779167313 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift @@ -160,7 +160,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, cutout: statusFrame.insetBy(dx: 8.0, dy: 8.0)) self.statusNode = statusNode self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode) - statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0)), animated: false, completion: {}) + statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0), animateRotation: true), animated: false, completion: {}) } if let statusNode = self.statusNode { statusNode.frame = statusFrame diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 1c42e81b23..9788809b1f 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -133,7 +133,16 @@ public struct PresentationResourcesItemList { public static func itemListReorderIndicatorIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListReorderIndicatorIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Item List/Reorder"), color: theme.list.controlSecondaryColor) + return generateImage(CGSize(width: 17.0, height: 14.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.itemBlocksSeparatorColor.cgColor) + + let lineHeight = 1.0 + UIScreenPixel + context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil)) + context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel + 6.0, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil)) + context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel + 12.0, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil)) + context.fillPath() + }) }) } diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index 9cca0bfd92..f2e5e38036 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -323,7 +323,7 @@ private final class ScrollContent: CombinedComponent { let infoBackground = infoBackground.update( component: RoundedRectangle( - color: theme.list.blocksBackgroundColor, + color: theme.overallDarkAppearance ? theme.list.itemModalBlocksBackgroundColor : theme.list.blocksBackgroundColor, cornerRadius: 10.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight), @@ -423,11 +423,13 @@ private final class ContainerComponent: CombinedComponent { let environment = context.environment[EnvironmentType.self] let state = context.state + let theme = environment.theme + let openContextMenu = context.component.openContextMenu let dismiss = context.component.dismiss - + let background = background.update( - component: Rectangle(color: environment.theme.list.plainBackgroundColor), + component: Rectangle(color: theme.overallDarkAppearance ? theme.list.modalBlocksBackgroundColor : theme.list.plainBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition @@ -694,9 +696,9 @@ public class AdsInfoScreen: ViewController { self.footerView = ComponentHostView() super.init() - + self.containerView.clipsToBounds = true - self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor + self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor self.addSubnode(self.dim) diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 3d7d4db84d..add64dbcdf 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -92,8 +92,8 @@ private final class SheetPageContent: CombinedComponent { let state = context.state let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let theme = presentationData.theme - let strings = presentationData.strings + let theme = environment.theme + let strings = environment.strings let sideInset: CGFloat = 16.0 + environment.safeInsets.left diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 38d171727a..26e1075791 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -684,6 +684,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private struct HighlightedState: Equatable { var quote: ChatInterfaceHighlightedState.Quote? + var todoTaskId: Int32? } private var highlightedState: HighlightedState? @@ -5903,7 +5904,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let highlightedStateValue = item.controllerInteraction.highlightedState { for (message, _) in item.content { if highlightedStateValue.messageStableId == message.stableId { - highlightedState = HighlightedState(quote: highlightedStateValue.quote) + highlightedState = HighlightedState(quote: highlightedStateValue.quote, todoTaskId: highlightedStateValue.todoTaskId) break } } @@ -5915,6 +5916,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true) + } else if let _ = contentNode as? ChatMessageTodoBubbleContentNode { + } } @@ -5998,6 +6001,34 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } }) + } else if highlightedState?.todoTaskId != nil { + Queue.mainQueue().after(0.3, { [weak self] in + guard let self, let _ = self.item, let backgroundHighlightNode = self.backgroundHighlightNode else { + return + } + + if let highlightedState = self.highlightedState, let todoTaskId = highlightedState.todoTaskId { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + var taskFrame: CGRect? + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode, let localFrame = contentNode.taskItemFrame(id: todoTaskId) { + taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview) + break + } + } + + if let taskFrame { + self.backgroundHighlightNode = nil + + backgroundHighlightNode.updateLayout(size: taskFrame.size, transition: transition) + transition.updateFrame(node: backgroundHighlightNode, frame: taskFrame) + backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.05, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in + backgroundHighlightNode?.removeFromSupernode() + }) + } + } + }) } } } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 88c6dd9b03..3c62390b8c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -1645,7 +1645,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media { let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027) - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true) } else { switch resourceStatus.mediaStatus { case var .fetchStatus(fetchStatus): @@ -1668,7 +1668,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { state = .check(appearance: nil) } else { - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true) } } case .Local: @@ -1712,7 +1712,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { switch resourceStatus.fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) - streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) + streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0), animateRotation: true) case .Local: streamingState = .none case .Remote, .Paused: @@ -1730,7 +1730,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } else if case .check = state { } else { let adjustedProgress: CGFloat = 0.027 - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0), animateRotation: true) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index f95da1047c..e9829eed60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1323,13 +1323,13 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { case let .Fetching(_, progress): if let isBuffering = isBuffering { if isBuffering { - state = .progress(value: nil, cancelEnabled: true, appearance: nil) + state = .progress(value: nil, cancelEnabled: true, appearance: nil, animateRotation: true) } else { state = .none } } else { let adjustedProgress = max(progress, 0.027) - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true) } case .Local: if isViewOnceMessage { @@ -1355,7 +1355,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { isLocal = true } if (isBuffering ?? false) && !isLocal { - state = .progress(value: nil, cancelEnabled: true, appearance: nil) + state = .progress(value: nil, cancelEnabled: true, appearance: nil, animateRotation: true) } else { state = .none } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 0fc2b9719f..8c606a3afa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1297,7 +1297,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { public func updateQuoteTextHighlightState(text: String?, offset: Int?, color: UIColor, animated: Bool) { var rectsSet: [CGRect] = [] if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { - let quoteRange = findQuoteRange(string: string, quoteText: text, offset: offset) if let quoteRange, let rects = cachedLayout.rangeRects(in: quoteRange)?.rects, !rects.isEmpty { rectsSet = rects diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index 7087dece29..fe2764c0d5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -1241,4 +1241,13 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode { } return nil } + + public func taskItemFrame(id: Int32) -> CGRect? { + for node in self.optionNodes { + if node.option?.id == id { + return node.frame + } + } + return nil + } } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index bfcc2f5bc4..f2d09c2b4a 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -31,10 +31,12 @@ public struct ChatInterfaceHighlightedState: Equatable { public let messageStableId: UInt32 public let quote: Quote? + public let todoTaskId: Int32? - public init(messageStableId: UInt32, quote: Quote?) { + public init(messageStableId: UInt32, quote: Quote?, todoTaskId: Int32?) { self.messageStableId = messageStableId self.quote = quote + self.todoTaskId = todoTaskId } } @@ -96,13 +98,15 @@ public struct NavigateToMessageParams { public var timestamp: Double? public var quote: Quote? + public var todoTaskId: Int32? public var progress: Promise? public var forceNew: Bool public var setupReply: Bool - public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil, forceNew: Bool = false, setupReply: Bool = false) { + public init(timestamp: Double?, quote: Quote?, todoTaskId: Int32? = nil, progress: Promise? = nil, forceNew: Bool = false, setupReply: Bool = false) { self.timestamp = timestamp self.quote = quote + self.todoTaskId = todoTaskId self.progress = progress self.forceNew = forceNew self.setupReply = setupReply diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index e49336f6e5..5289ed01b9 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -109,6 +109,9 @@ final class ComposeTodoScreenComponent: Component { private var currentEditingTag: AnyObject? + private var reorderRecognizer: ReorderGestureRecognizer? + private var reorderingItem: (id: AnyHashable, snapshotView: UIView, backgroundView: UIView, initialPosition: CGPoint, position: CGPoint)? + var isAppendableByOthers = false var isCompletableByOthers = false @@ -129,6 +132,39 @@ final class ComposeTodoScreenComponent: Component { self.scrollView.delegate = self self.addSubview(self.scrollView) + + let reorderRecognizer = ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let self, let (id, item) = self.item(at: point) else { + return (allowed: false, requiresLongPress: false, id: nil, item: nil) + } + return (allowed: true, requiresLongPress: false, id: id, item: item) + }, + willBegin: { point in + }, + began: { [weak self] item in + guard let self else { + return + } + self.setReorderingItem(item: item) + }, + ended: { [weak self] in + guard let self else { + return + } + self.setReorderingItem(item: nil) + }, + moved: { [weak self] distance in + guard let self else { + return + } + self.moveReorderingItem(distance: distance) + }, + isActiveUpdated: { _ in + } + ) + self.reorderRecognizer = reorderRecognizer + self.addGestureRecognizer(reorderRecognizer) } required init?(coder: NSCoder) { @@ -143,6 +179,124 @@ final class ComposeTodoScreenComponent: Component { self.scrollView.setContentOffset(CGPoint(), animated: true) } + private func item(at point: CGPoint) -> (AnyHashable, ComponentView)? { + let localPoint = self.todoItemsSectionContainer.convert(point, from: self) + for (id, itemView) in self.todoItemsSectionContainer.itemViews { + if let view = itemView.contents.view { + let viewFrame = view.convert(view.bounds, to: self.todoItemsSectionContainer) + let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height)) + if iconFrame.contains(localPoint) { + return (id, itemView.contents) + } + } + } + return nil + } + + func setReorderingItem(item: AnyHashable?) { + guard let environment = self.environment else { + return + } + var mappedItem: (AnyHashable, ComponentView)? + for (id, itemView) in self.todoItemsSectionContainer.itemViews { + if id == item { + mappedItem = (id, itemView.contents) + break + } + } + if self.reorderingItem?.id != mappedItem?.0 { + if let (id, visibleItem) = mappedItem, let view = visibleItem.view, !view.isHidden, let viewSuperview = view.superview, let snapshotView = view.snapshotView(afterScreenUpdates: false) { + let mappedCenter = viewSuperview.convert(view.center, to: self.scrollView) + + let wrapperView = UIView() + wrapperView.alpha = 0.8 + wrapperView.frame = CGRect(origin: mappedCenter.offsetBy(dx: -snapshotView.bounds.width / 2.0, dy: -snapshotView.bounds.height / 2.0), size: snapshotView.bounds.size) + + let theme = environment.theme.withModalBlocksBackground() + let backgroundView = UIImageView(image: generateReorderingBackgroundImage(backgroundColor: theme.list.itemBlocksBackgroundColor)) + backgroundView.frame = wrapperView.bounds.insetBy(dx: -10.0, dy: -10.0) + snapshotView.frame = snapshotView.bounds + + wrapperView.addSubview(backgroundView) + wrapperView.addSubview(snapshotView) + + backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + wrapperView.transform = CGAffineTransformMakeScale(1.04, 1.04) + wrapperView.layer.animateScale(from: 1.0, to: 1.04, duration: 0.2) + + self.scrollView.addSubview(wrapperView) + self.reorderingItem = (id, wrapperView, backgroundView, mappedCenter, mappedCenter) + self.state?.updated() + } else { + if let reorderingItem = self.reorderingItem { + self.reorderingItem = nil + for (itemId, itemView) in self.todoItemsSectionContainer.itemViews { + if itemId == reorderingItem.id, let view = itemView.contents.view { + let viewFrame = view.convert(view.bounds, to: self) + let transition = ComponentTransition.spring(duration: 0.3) + transition.setPosition(view: reorderingItem.snapshotView, position: viewFrame.center) + transition.setAlpha(view: reorderingItem.backgroundView, alpha: 0.0, completion: { _ in + reorderingItem.snapshotView.removeFromSuperview() + self.state?.updated() + }) + transition.setScale(view: reorderingItem.snapshotView, scale: 1.0) + break + } + } + } + } + } + } + + func moveReorderingItem(distance: CGPoint) { + if let (id, snapshotView, backgroundView, initialPosition, _) = self.reorderingItem { + let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y) + self.reorderingItem = (id, snapshotView, backgroundView, initialPosition, targetPosition) + + snapshotView.center = targetPosition + + for (itemId, itemView) in self.todoItemsSectionContainer.itemViews { + if itemId == id { + continue + } + if let view = itemView.contents.view { + let viewFrame = view.convert(view.bounds, to: self) + if viewFrame.contains(targetPosition) { + if let targetIndex = self.todoItems.firstIndex(where: { AnyHashable($0.id) == itemId }), let reorderingItem = self.todoItems.first(where: { AnyHashable($0.id) == id }) { + self.reorderIfPossible(item: reorderingItem, toIndex: targetIndex) + } + break + } + } + } + } + } + + private func reorderIfPossible(item: TodoItem, toIndex: Int) { + guard let component = self.component else { + return + } + let targetItem = self.todoItems[toIndex] + guard targetItem.textInputState.hasText else { + return + } + var canEdit = true + if let _ = component.initialData.existingTodo, !component.initialData.canEdit { + canEdit = false + } + if !canEdit, let existingTodo = component.initialData.existingTodo, existingTodo.items.contains(where: { $0.id == targetItem.id }) { + return + } + if let fromIndex = self.todoItems.firstIndex(where: { $0.id == item.id }) { + self.todoItems[toIndex] = item + self.todoItems[fromIndex] = targetItem + + HapticFeedback().tap() + + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + func validatedInput() -> TelegramMediaTodo? { if self.todoTextInputState.text.length == 0 { return nil @@ -432,6 +586,8 @@ final class ComposeTodoScreenComponent: Component { let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment + let theme = environment.theme.withModalBlocksBackground() + let isFirstTime = self.component == nil if self.component == nil { if let existingTodo = component.initialData.existingTodo { @@ -599,7 +755,7 @@ final class ComposeTodoScreenComponent: Component { let sectionSpacing: CGFloat = 24.0 if themeUpdated { - self.backgroundColor = environment.theme.list.blocksBackgroundColor + self.backgroundColor = theme.list.blocksBackgroundColor } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -617,7 +773,7 @@ final class ComposeTodoScreenComponent: Component { todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.todoTextInputState, context: component.context, - theme: environment.theme, + theme: theme, strings: environment.strings, isEnabled: canEdit, resetText: self.resetTodoText.flatMap { resetText in @@ -625,7 +781,6 @@ final class ComposeTodoScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag, characterLimit: component.initialData.maxTodoTextLength, - canReorder: canEdit, emptyLineHandling: .allowed, returnKeyAction: { [weak self] in guard let self else { @@ -661,7 +816,7 @@ final class ComposeTodoScreenComponent: Component { let todoTextSectionSize = self.todoTextSection.update( transition: transition, component: AnyComponent(ListSectionComponent( - theme: environment.theme, + theme: theme, header: nil, footer: nil, items: todoTextSectionItems @@ -701,7 +856,7 @@ final class ComposeTodoScreenComponent: Component { todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent( externalState: todoItem.textInputState, context: component.context, - theme: environment.theme, + theme: theme, strings: environment.strings, isEnabled: isEnabled, resetText: todoItem.resetText.flatMap { resetText in @@ -709,6 +864,7 @@ final class ComposeTodoScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === todoItem.textFieldTag, characterLimit: component.initialData.maxTodoItemLength, + canReorder: isEnabled, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { @@ -774,7 +930,7 @@ final class ComposeTodoScreenComponent: Component { self.todoItemsSectionContainer.itemViews[itemId] = itemView itemView.contents.parentState = state } - + let itemSize = itemView.contents.update( transition: itemTransition, component: item.component, @@ -788,6 +944,12 @@ final class ComposeTodoScreenComponent: Component { size: itemSize, transition: itemTransition )) + + var isReordering = false + if let reorderingItem = self.reorderingItem, itemId == reorderingItem.id { + isReordering = true + } + itemView.contents.view?.isHidden = isReordering } for i in 0 ..< self.todoItems.count { @@ -836,7 +998,7 @@ final class ComposeTodoScreenComponent: Component { let todoItemsSectionUpdateResult = self.todoItemsSectionContainer.update( configuration: ListSectionContentView.Configuration( - theme: environment.theme, + theme: theme, displaySeparators: true, extendsItemHighlightToSection: false, background: .all @@ -854,7 +1016,7 @@ final class ComposeTodoScreenComponent: Component { text: .plain(NSAttributedString( string: "TO DO LIST", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), @@ -900,19 +1062,19 @@ final class ComposeTodoScreenComponent: Component { } let todoItemsComponent: AnyComponent - if !"".isEmpty, todoItemsLimitReached { + if todoItemsLimitReached { todoItemsFooterTransition = todoItemsFooterTransition.withAnimation(.none) let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize) let boldTextFont = Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize) - let textColor = environment.theme.list.freeTextColor + let textColor = theme.list.freeTextColor todoItemsComponent = AnyComponent(MultilineTextComponent( text: .markdown( - text: "Limit of tasks reached. You can increase the limit to **20 tasks** by subscribing to [Telegram Premium]().", + text: "Maximum number of tasks reached.", attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), - link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor), + link: MarkdownAttributeSet(font: textFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } @@ -969,7 +1131,7 @@ final class ComposeTodoScreenComponent: Component { todoItemsComponent = AnyComponent(AnimatedTextComponent( font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - color: environment.theme.list.freeTextColor, + color: theme.list.freeTextColor, items: todoItemsFooterItems )) } @@ -1004,13 +1166,13 @@ final class ComposeTodoScreenComponent: Component { var todoSettingsSectionItems: [AnyComponentWithIdentity] = [] if canEdit && component.peer.id != component.context.account.peerId { todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "completable", component: AnyComponent(ListActionItemComponent( - theme: environment.theme, + theme: theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "Allow Others to Mark as Done", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), @@ -1027,13 +1189,13 @@ final class ComposeTodoScreenComponent: Component { if self.isCompletableByOthers { todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "editable", component: AnyComponent(ListActionItemComponent( - theme: environment.theme, + theme: theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "Allow Others to Add Tasks", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), @@ -1054,7 +1216,7 @@ final class ComposeTodoScreenComponent: Component { let todoSettingsSectionSize = self.todoSettingsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( - theme: environment.theme, + theme: theme, header: nil, footer: nil, items: todoSettingsSectionItems @@ -1164,7 +1326,7 @@ final class ComposeTodoScreenComponent: Component { component: AnyComponent(EmojiSuggestionsComponent( context: component.context, userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), + theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor), animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, files: value, @@ -1530,3 +1692,212 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont return true } } + +private final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?) + private let willBegin: (CGPoint) -> Void + private let began: (AnyHashable) -> Void + private let ended: () -> Void + private let moved: (CGPoint) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var id: AnyHashable? + private var itemView: ComponentView? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + + self.isActiveUpdated(false) + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let id = self.id { + self.began(id) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.id = id + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let id = self.id { + self.began(id) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} + +private func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath) + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor) + context.setFillColor(backgroundColor.cgColor) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32) +} diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index 3989ee04fe..86fb2f86b0 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -257,6 +257,7 @@ public final class ListComposePollOptionComponent: Component { private let textField = ComponentView() private var modeSelector: ComponentView? + private var reorderIconView: UIImageView? private var checkView: CheckView? @@ -454,6 +455,43 @@ public final class ListComposePollOptionComponent: Component { checkView?.removeFromSuperview() }) } + + var rightIconsInset: CGFloat = 0.0 + if component.canReorder, let externalState = component.externalState, externalState.hasText { + var reorderIconTransition = transition + let reorderIconView: UIImageView + if let current = self.reorderIconView { + reorderIconView = current + } else { + reorderIconTransition = reorderIconTransition.withAnimation(.none) + reorderIconView = UIImageView() + self.reorderIconView = reorderIconView + self.addSubview(reorderIconView) + } + reorderIconView.image = PresentationResourcesItemList.itemListReorderIndicatorIcon(component.theme) + + var reorderIconSize = CGSize() + if let icon = reorderIconView.image { + reorderIconSize = icon.size + } + + let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize) + reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center) + reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size)) + + rightIconsInset += 36.0 + } else if let reorderIconView = self.reorderIconView { + self.reorderIconView = nil + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: reorderIconView, alpha: 0.0, completion: { [weak reorderIconView] _ in + reorderIconView?.removeFromSuperview() + }) + alphaTransition.setScale(view: reorderIconView, scale: 0.001) + } else { + reorderIconView.removeFromSuperview() + } + } if let inputMode = component.inputMode { var modeSelectorTransition = transition @@ -501,7 +539,7 @@ public final class ListComposePollOptionComponent: Component { environment: {}, containerSize: modeSelectorSize ) - let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) + let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 97f8d32d35..9a322ed385 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -336,7 +336,7 @@ public final class StoryFooterPanelComponent: Component { statusNode.view.frame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - statusSize.height) * 0.5)), size: statusSize) innerLeftOffset += statusSize.width + 10.0 - statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0))) + statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0), animateRotation: true)) let uploadingTextSize = uploadingText.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift index bedee031cf..b534641c06 100644 --- a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift +++ b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift @@ -9,20 +9,17 @@ public final class ToastContentComponent: Component { public let content: AnyComponent public let insets: UIEdgeInsets public let iconSpacing: CGFloat - public let action: (() -> Void)? public init( icon: AnyComponent, content: AnyComponent, insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0), - iconSpacing: CGFloat = 10.0, - action: (() -> Void)? = nil + iconSpacing: CGFloat = 10.0 ) { self.icon = icon self.content = content self.insets = insets self.iconSpacing = iconSpacing - self.action = action } public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool { @@ -69,18 +66,9 @@ public final class ToastContentComponent: Component { } - @objc private func tapped() { - self.component?.action?() - } - func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var contentHeight: CGFloat = 0.0 - if self.component == nil { - if let _ = component.action { - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) - } - } self.component = component let leftInset: CGFloat = component.insets.left diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 94eb9e5071..d38fb8b18b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4766,7 +4766,7 @@ extension ChatControllerImpl { } } - let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }) + let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: toSubject.todoTaskId) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: []) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 9b11b9cd17..c43ba3f5f4 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -340,13 +340,15 @@ extension ChatControllerImpl { } var quote: (string: String, offset: Int?)? + var todoTaskId: Int32? var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } setupReply = params.setupReply + todoTaskId = params.todoTaskId } - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition, setupReply: setupReply) + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, todoTaskId: todoTaskId, scrollPosition: scrollPosition, setupReply: setupReply) if delayCompletion { Queue.mainQueue().after(0.25, { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index badb2c55e6..6f31174cd7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -970,7 +970,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil))) + var todoTaskId: Int32? + if case let .todoCompletions(completed, incompleted) = action.action { + if let completedTaskId = completed.first { + todoTaskId = completedTaskId + } else if let incompletedTaskId = incompleted.first { + todoTaskId = incompletedTaskId + } + } + self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, todoTaskId: todoTaskId))) break } } @@ -4840,7 +4848,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - self.dismissAllUndoControllers() + self.dismissAllTooltips() //TODO:localize if !self.context.isPremium { let controller = UndoOverlayController( diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 4b354ef935..3ad19353ff 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -3340,8 +3340,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, todoTaskId: Int32? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? {