diff --git a/build-system/template_minimal_development_configuration.json b/build-system/template_minimal_development_configuration.json index 1aad0aed955..6fdfb507347 100755 --- a/build-system/template_minimal_development_configuration.json +++ b/build-system/template_minimal_development_configuration.json @@ -1,8 +1,8 @@ { - "bundle_id": "org.{! a random string !}.Telegram", - "api_id": "{! get one at https://my.telegram.org/apps !}", - "api_hash": "{! get one at https://my.telegram.org/apps !}", - "team_id": "{! check README.md !}", + "bundle_id": "org.31375bc829c0233d.Telegraph", + "api_id": "8", + "api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb", + "team_id": "JTVJ6L7R86", "app_center_id": "0", "is_internal_build": "true", "is_appstore_build": "false", diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index d7f0ccc244f..803c402f84c 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -559,6 +559,17 @@ public enum ChatControllerPresentationMode: Equatable { case inline(NavigationController?) } +public final class ChatListPreviewPresentationData { + + public let sourceNodeAndRect: (() -> (ASDisplayNode, CGRect)?) + public let contentArea: (() -> (CGRect)) + + public init(sourceNodeAndRect: @escaping (() -> (ASDisplayNode, CGRect)?), contentArea: @escaping (() -> (CGRect))) { + self.sourceNodeAndRect = sourceNodeAndRect + self.contentArea = contentArea + } +} + public enum ChatPresentationInputQueryResult: Equatable { case stickers([FoundStickerItem]) case hashtags([String]) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 7a39ef44d19..393960634cf 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -92,10 +92,11 @@ private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, i } else if case let .archivedChatsIcon(hiddenByDefault) = icon, let theme = theme { let backgroundColors: (UIColor, UIColor) if hiddenByDefault { - backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors - } else { - backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors - } + print("hidden by default") +// backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors + } //else { + backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors +// } colors = [backgroundColors.1, backgroundColors.0] } else { colors = AvatarNode.grayscaleColors @@ -348,12 +349,13 @@ public final class AvatarNode: ASDisplayNode { if let overrideImage = self.overrideImage, case let .archivedChatsIcon(hiddenByDefault) = overrideImage { let backgroundColors: (UIColor, UIColor) if hiddenByDefault { - backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors - iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor - } else { +// backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors +// iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor + print("archieve hidden by default") + } //else { backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor - } +// } let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor] backgroundColor = backgroundColors.1.mixedWith(backgroundColors.0, alpha: 0.5) animationBackgroundNode.image = generateGradientFilledCircleImage(diameter: self.imageNode.frame.width, colors: colors) @@ -566,10 +568,11 @@ public final class AvatarNode: ASDisplayNode { if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none { if case let .archivedChatsIcon(hiddenByDefault) = parameters.icon, let theme = parameters.theme { if hiddenByDefault { - iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor - } else { +// iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor + + } //else { iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor - } +// } } } diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index a8c8cf10284..797280e7e0b 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -99,6 +99,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", "//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen", + "//submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 508a43dfe2f..4a1f11781be 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1098,10 +1098,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if !didDisplayTip { - #if DEBUG - #else +// #if DEBUG +// #else let _ = ApplicationSpecificNotice.setDisplayChatListArchiveTooltip(accountManager: self.context.sharedContext.accountManager).start() - #endif +// #endif self.push(ArchiveInfoScreen(context: self.context, settings: settings)) } @@ -1289,6 +1289,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + switch item.content { case let .groupReference(groupReference): let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupReference.groupId), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) @@ -1304,15 +1305,31 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case .chatList: if case let .channel(channel) = peer.peer, channel.flags.contains(.isForum) { if let threadId = threadId { - let source: ContextContentSource +// let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - strongSelf.presentInGlobalOverlay(contextController) + + + strongSelf.present(chatController, in: .window(.root), with: ChatListPreviewPresentationData(sourceNodeAndRect: { + return (node, node.frame) + }, contentArea: { + let baseContentFrame = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.frame + if case let .known(topOffset) = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.visibleContentOffset(), + case let .known(bottomOffset) = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.visibleBottomContentOffset() { + return baseContentFrame.inset(by: .init(top: topOffset, left: .zero, bottom: bottomOffset, right: .zero)) + } else { + return baseContentFrame.inset(by: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.headerInsets) + } + })) + +// source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) + + +// let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) +// strongSelf.presentInGlobalOverlay(contextController) } else { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master @@ -1320,17 +1337,34 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.presentInGlobalOverlay(contextController) } } else { - let source: ContextContentSource - if let location = location { - source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) - } else { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) - chatController.canReadHistory.set(false) - source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - } +// let source: ContextContentSource +// if let location = location { +// source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) +// } else { +// let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) +// chatController.canReadHistory.set(false) +// strongSelf.present(chatController, in: .window(.root), with: ChatListPreviewPresentationData(sourceNodeAndRect: { +// return (node, node.frame) +// })) + + let chatPreviewController = ChatListPreviewController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId)) + strongSelf.present(chatPreviewController, in: .window(.root), with: ChatListPreviewPresentationData(sourceNodeAndRect: { + return (node, node.frame) + }, contentArea: { + let baseContentFrame = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.frame + if case let .known(topOffset) = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.visibleContentOffset(), + case let .known(bottomOffset) = strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.visibleBottomContentOffset() { + return baseContentFrame.inset(by: .init(top: topOffset, left: .zero, bottom: bottomOffset, right: .zero)) + } else { + return baseContentFrame.inset(by: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.headerInsets) + } + })) + +// source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) +// } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - strongSelf.presentInGlobalOverlay(contextController) +// let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) +// strongSelf.presentInGlobalOverlay(contextController) } case let .forum(pinnedIndex, _, threadId, _, _): let isPinned: Bool @@ -4648,6 +4682,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if updatedValue { state.hiddenItemShouldBeTemporaryRevealed = false } + state.archiveParams.updateVisibility(isRevealed: state.hiddenItemShouldBeTemporaryRevealed, + isHiddenByDefault: updatedValue) state.peerIdWithRevealedOptions = nil return state } @@ -5117,6 +5153,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.chatListDisplayNode.effectiveContainerNode.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false + state.archiveParams.updateVisibility(isRevealed: false) return state } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index bf9c314aca1..a057da7b836 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -242,7 +242,7 @@ private final class ChatListShimmerNode: ASDisplayNode { topForumTopicItems: [], autoremoveTimeout: nil, storyState: nil - )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, params: .emptyVisibleParams, interaction: interaction) } var itemNodes: [ChatListItemNode] = [] @@ -2428,9 +2428,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { if listView.isDragging { var overscrollSelectedId: EnginePeer.Id? var overscrollHiddenChatItemsAllowed = false + var overscrollFraction: CGFloat? + var currentFraction: CGFloat? if let controller = self.controller, let componentView = controller.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() { overscrollSelectedId = storyPeerListView.overscrollSelectedId overscrollHiddenChatItemsAllowed = storyPeerListView.overscrollHiddenChatItemsAllowed + overscrollFraction = storyPeerListView.overscrollFraction + currentFraction = storyPeerListView.currentFraction } if let chatListNode = listView as? ChatListNode { @@ -2471,26 +2475,125 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { manuallyAllow = true } - if manuallyAllow, case let .known(value) = offset, value + listView.tempTopInset <= -40.0 { - overscrollHiddenChatItemsAllowed = true + + if manuallyAllow, case let .known(value) = offset { + let difference = value + listView.tempTopInset - 40.0 + print("offset difference: \(difference)") + if difference <= 0 { + overscrollHiddenChatItemsAllowed = true + } } } - if overscrollHiddenChatItemsAllowed { - if self.allowOverscrollItemExpansion { - let timestamp = CACurrentMediaTime() - if let _ = self.currentOverscrollItemExpansionTimestamp { - } else { - self.currentOverscrollItemExpansionTimestamp = timestamp - } - - if let currentOverscrollItemExpansionTimestamp = self.currentOverscrollItemExpansionTimestamp, currentOverscrollItemExpansionTimestamp <= timestamp - 0.0 { +// if self.allowOverscrollItemExpansion { +// if overscrollHiddenChatItemsAllowed { +// let timestamp = CACurrentMediaTime() +// if let _ = self.currentOverscrollItemExpansionTimestamp { +// } else { +// self.currentOverscrollItemExpansionTimestamp = timestamp +// } +// +// if let currentOverscrollItemExpansionTimestamp = self.currentOverscrollItemExpansionTimestamp, currentOverscrollItemExpansionTimestamp <= timestamp - 0.0 { +// self.allowOverscrollItemExpansion = false +// +// if isPrimary { +// self.mainContainerNode.currentItemNode.revealScrollHiddenItem() +// } else { +// self.inlineStackContainerNode?.currentItemNode.revealScrollHiddenItem() +// } +// } +// } +// } + + +// if case let .known(value) = offset { +// let listViewOffsetFraction = (value + listView.tempTopInset)/76 +// print("#1 listViewOffset: \(value) listViewOffsetFraction: \(listViewOffsetFraction)") +// } +// let scrollViewFraction = (listView.scroller.contentOffset.y - listView.tempTopInset) / 76 +// print("#1 overscrollFraction: \(overscrollFraction ?? .zero) scrollViewOffset: \(listView.scroller.contentOffset) topInset: \(listView.tempTopInset) scrollViewFraction: \(scrollViewFraction)") +// print("###1 \noverscrollFraction: \(overscrollFraction ?? .zero) \nscrollViewOffset: \(listView.scroller.contentOffset) \ntopInset: \(listView.tempTopInset) \nscrollViewFraction: \(scrollViewFraction)") +// print("###2 currentFraction: \(currentFraction ?? .zero) \ncurrentActivityFraction: \(currentActivityFraction ?? .zero)") + + + var archiveFraction: CGFloat = 0.0 + + let currentParams = self.mainContainerNode.currentItemNode.currentState.archiveParams + guard !currentParams.isArchiveGroupVisible else { return } + + if let overscrollFraction, let currentFraction { + archiveFraction = (overscrollFraction / 0.5) * 0.8 + if currentFraction < 0 { + let lastOverscrol = max(-1.0, min(1.0, (currentFraction / -0.2))) * 0.2 + archiveFraction += lastOverscrol + archiveFraction = max(-1.0, archiveFraction) + archiveFraction = min(1.0, archiveFraction) + } else if currentFraction >= 1.0 { + archiveFraction = -currentFraction + } + } + if + archiveFraction != 0, +// self.allowOverscrollItemExpansion, + let node = self.mainContainerNode.currentItemNode.itemNodeAtIndex(2) as? ChatListItemNode, node.isNodeLoaded, + var itemHeight = node.currentItemHeight, itemHeight > 0 { +// if !self.mainContainerNode.currentItemNode.currentState.archiveParams.finalizeAnimation { + itemHeight *= 1.2 +// } + + let expandedHeight: CGFloat + if archiveFraction < 0 { + expandedHeight = itemHeight - (-archiveFraction * itemHeight) + } else { + expandedHeight = archiveFraction * itemHeight + } +//listView.tempTopInset - 40.0 +// print("expandedHeight: \(expandedHeight) overscrollFraction: \(overscrollFraction) itemHeight: \(itemHeight)") + + let timestamp = CACurrentMediaTime() + if let _ = self.currentOverscrollItemExpansionTimestamp { + } else { + self.currentOverscrollItemExpansionTimestamp = timestamp + } + + var scrollOffset = listView.scroller.contentOffset.y + if case let .known(value) = offset { + scrollOffset = value + } + + if let currentOverscrollItemExpansionTimestamp = self.currentOverscrollItemExpansionTimestamp, currentOverscrollItemExpansionTimestamp <= timestamp - 0.0 { + if archiveFraction >= 0.8 { self.allowOverscrollItemExpansion = false - - if isPrimary { - self.mainContainerNode.currentItemNode.revealScrollHiddenItem() - } else { - self.inlineStackContainerNode?.currentItemNode.revealScrollHiddenItem() + } + if isPrimary { + self.mainContainerNode.currentItemNode.forEachItemNode { node in + if let chatNode = node as? ChatListItemNode { + if case let .groupReference(data) = chatNode.item?.content, data.groupId == .archive, expandedHeight != chatNode.item?.params.expandedHeight { +// print("expandedHeight: \(expandedHeight) archiveFraction: \(archiveFraction) itemHeight: \(itemHeight)") + self.mainContainerNode.currentItemNode.updateArchiveParams(params: .init( + scrollOffset: scrollOffset.rounded(), + storiesFraction: archiveFraction, + expandedHeight: expandedHeight, + finalizeAnimation: false, + isHiddenByDefault: currentParams.isHiddenByDefault + )) + } + } + } + } else { + self.inlineStackContainerNode?.currentItemNode.forEachItemNode { node in + if let chatNode = node as? ChatListItemNode { + if case let .groupReference(data) = chatNode.item?.content, data.groupId == .archive, expandedHeight != chatNode.item?.params.expandedHeight { +// print("expandedHeight: \(expandedHeight) archiveFraction: \(archiveFraction) itemHeight: \(itemHeight)") + self.inlineStackContainerNode?.currentItemNode.updateArchiveParams(params: .init( + scrollOffset: scrollOffset.rounded(), + storiesFraction: archiveFraction, + expandedHeight: expandedHeight, + finalizeAnimation: false, + isHiddenByDefault: currentParams.isHiddenByDefault + )) + } + } } } } @@ -2544,6 +2647,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } self.allowOverscrollItemExpansion = false self.currentOverscrollItemExpansionTimestamp = nil + + let params = self.mainContainerNode.currentItemNode.currentState.archiveParams + self.mainContainerNode.currentItemNode.updateArchiveParams(params: params.withUpdatedFinalizeAnimation(true)) } private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool { @@ -2555,7 +2661,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else { return false } - + if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { let searchScrollOffset = clippedScrollOffset if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight { diff --git a/submodules/ChatListUI/Sources/ChatListPreviewController.swift b/submodules/ChatListUI/Sources/ChatListPreviewController.swift new file mode 100644 index 00000000000..efc2faac7fd --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListPreviewController.swift @@ -0,0 +1,845 @@ +// +// ChatListPreviewController.swift +// ChatListUI +// +// Created by Bogdan Redkin on 02/09/2023. +// + +import AccountContext +import AsyncDisplayKit +import ChatAvatarNavigationNode +import ChatTitleView +import ContextUI +import Display +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import UIKit + +final class ChatListPreviewContentContainerNode: ASDisplayNode { + public var controllerNode: ContextControllerContentNode? + + override public init() { + super.init() + } + + public func updateLayout(size: CGSize, scaledSize: CGSize, transition: ContainedViewLayoutTransition) { + guard let contentNode = self.controllerNode else { return } + transition.updatePosition(node: contentNode, position: CGPoint(x: scaledSize.width / 2.0, y: scaledSize.height / 2.0)) + transition.updateBounds(node: contentNode, bounds: CGRect(origin: CGPoint(), size: size)) + transition.updateTransformScale(node: contentNode, scale: scaledSize.width / size.width) + contentNode.updateLayout(size: size, transition: transition) + contentNode.controller.containerLayoutUpdated( + ContainerViewLayout( + size: size, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + deviceMetrics: .iPhoneX, + intrinsicInsets: UIEdgeInsets(), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ), + transition: transition + ) + } +} + +func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { + let sourceWindowFrame = fromView.convert(frame, to: nil) + var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) + + if let fromWindow = fromView.window, let toWindow = toView.window { + targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width + } + return targetWindowFrame +} + +final class ChatListPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + private let presentationData: PresentationData + + private var validLayout: ContainerViewLayout? + + private let effectView: UIVisualEffectView + private var propertyAnimator: AnyObject? + private var displayLinkAnimator: DisplayLinkAnimator? + private let dimNode: ASDisplayNode + private let withoutBlurDimNode: ASDisplayNode + private let dismissNode: ASDisplayNode + private let dismissAccessibilityArea: AccessibilityAreaNode + private let clippingNode: ASDisplayNode + private let scrollNode: ASScrollNode + + private var originalProjectedContentViewFrame: (CGRect, CGRect)? +// private var contentAreaInScreenSpace: CGRect? +// private var customPosition: CGPoint? + private let contentContainerNode: ChatListPreviewContentContainerNode + private weak var gesture: ContextGesture? + + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + private var animatedIn = false + private var isAnimatingOut = false + private var didCompleteAnimationIn = false + + var presentationArguments: ChatListPreviewPresentationData? + var controller: ViewController? + + init(context: AccountContext, gesture: ContextGesture?) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.gesture = gesture + + self.effectView = UIVisualEffectView() + if #available(iOS 9.0, *) { + } else { + if presentationData.theme.rootController.keyboardColor == .dark { + self.effectView.effect = UIBlurEffect(style: .dark) + } else { + self.effectView.effect = UIBlurEffect(style: .light) + } + self.effectView.alpha = 0.0 + } + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.dimNode.alpha = 0.0 + + self.withoutBlurDimNode = ASDisplayNode() + self.withoutBlurDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + self.withoutBlurDimNode.alpha = 0.0 + + self.dismissNode = ASDisplayNode() + self.dismissAccessibilityArea = AccessibilityAreaNode() + self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu + self.dismissAccessibilityArea.accessibilityTraits = .button + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollNode = ASScrollNode() + self.scrollNode.canCancelAllTouchesInViews = true + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.contentContainerNode = ChatListPreviewContentContainerNode() + + super.init() + self.scrollNode.view.delegate = self + + self.view.addSubview(self.effectView) + self.addSubnode(self.dimNode) + self.addSubnode(self.withoutBlurDimNode) + + self.addSubnode(self.clippingNode) + + self.clippingNode.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.dismissNode) + self.scrollNode.addSubnode(self.dismissAccessibilityArea) + + self.initializeContent() + + self.dismissAccessibilityArea.activate = { [weak self] in + self?.dimNodeTapped() + return true + } + } + + deinit { + if let propertyAnimator = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + } + } + + override func didLoad() { + super.didLoad() + + self.dismissNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapped))) + } + + @objc private func dimNodeTapped() { + guard self.animatedIn else { + return + } + self.cancel?() + } + + func initializeContent() { + guard + let presentationArguments, + let controller, + let (sourceNode, sourceNodeRect) = presentationArguments.sourceNodeAndRect() + else { return } + + let controlleContentrNode = ContextControllerContentNode(sourceView: sourceNode.view, controller: controller, tapped: { + print("tapped") + }) + + self.contentContainerNode.controllerNode = controlleContentrNode + self.scrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = 14.0 + self.contentContainerNode.addSubnode(controlleContentrNode) + + let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + func animateIn() { + self.gesture?.endPressedAppearance() +// guard +// let tp = transitionParams +// else { return } +// let sourceTitleSnapshot = transitionParams.sourceTitleSnapshot +// let sourceChatItemSnapshot = tp.sourceChatItemSnapshot +// let sourceAvatarSnapshot = transitionParams.sourceAvatarSnapshot + + if let validLayout = self.validLayout { + self.updateLayout(validLayout, transition: .immediate) + } + + if !self.dimNode.isHidden { + self.dimNode.alpha = 1.0 + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } else { + self.withoutBlurDimNode.alpha = 1.0 + self.withoutBlurDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } +// +// if let propertyAnimator = self.propertyAnimator { +// let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator +// propertyAnimator?.stopAnimation(true) +// } +// self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) +// self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) +// self.propertyAnimator = UIViewPropertyAnimator(duration: 0.3 * UIView.animationDurationFactor(), curve: .easeInOut, animations: { +// }) +// + +// let springDuration: Double = 0.52 +// let springDamping: CGFloat = 110.0 + +// self.contentContainerNode.allowsGroupOpacity = true +// self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 1.15, completion: { [weak self] _ in +// self?.contentContainerNode.allowsGroupOpacity = false +// }) + +// sourceAvatarSnapshot.animateAlpha(from: 1.0, to: 0.0, duration: 1.15) +// sourceChatItemSnapshot.animateAlpha(from: 1.0, to: 0.0, duration: 1.15) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert( + CGRect( + origin: CGPoint( + x: originalProjectedContentViewFrame.1.minX, + y: originalProjectedContentViewFrame.1.minY + ), + size: CGSize( + width: originalProjectedContentViewFrame.1.width, + height: originalProjectedContentViewFrame.1.height + ) + ), + to: self.scrollNode.view + ) + + print("locate source frame: \(localSourceFrame) originalProjectedContentViewFrame: \(originalProjectedContentViewFrame)") + + CATransaction.begin() + CATransaction.setCompletionBlock { + print("animation finsihed") + } + CATransaction.setAnimationDuration(0.5) + + if let _ = self.propertyAnimator {` + self.displayLinkAnimator = DisplayLinkAnimator(duration: 1.15 * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { [weak self] in + self?.didCompleteAnimationIn = true + }) + } else { + UIView.animate(withDuration: 1.15, animations: { + self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) + }, completion: { [weak self] _ in + self?.didCompleteAnimationIn = true + // self?.actionsContainerNode.animateIn() + }) + } + CATransaction.commit() + //prepare variables to transition snapshots + guard + let controller = self.contentContainerNode.controllerNode, + let (sourceNode, sourceFrame) = self.presentationArguments?.sourceNodeAndRect(), + let contentArea = self.presentationArguments?.contentArea(), + let sourceChatItem = sourceNode.supernode as? ChatListItemNode else { return } + + let titleNode = sourceChatItem.titleNode + let titleSnapshot = titleNode.view.snapshotContentTree() + let titleSourceframe = self.view.convert(titleNode.view.frame, from: titleNode.view) + + let avatarNode = sourceChatItem.avatarContainerNode + let avatarSnapshot = avatarNode.view.snapshotContentTree() + let avatarSourceFrame = self.view.convert(avatarNode.view.frame, from: avatarNode.view) + + titleNode.isHidden = true + avatarNode.isHidden = true + + guard + let chatSnapshot = sourceChatItem.view.snapshotContentTree(), + let titleView = controller.controller.navigationItem.titleView as? ChatTitleView, + let navigationBackgroundSnapshot = controller.controller.navigationBar?.layer.snapshotContentTree(), + let titleSnapshot, + let avatarSnapshot + else { + titleNode.isHidden = false + avatarNode.isHidden = false + return + } + + titleNode.isHidden = false + avatarNode.isHidden = false + + + // snapshot transition and resize + CATransaction.begin() + CATransaction.setAnimationDuration(0.35) + + let targeBgPosition = CGPoint(x: self.contentContainerNode.frame.midX, y: self.contentContainerNode.frame.minY + localSourceFrame.height / 2.0) + _ = self.contentContainerNode.frame.size + + let sourceMessageFrameStart = self.view.convert(sourceFrame, from: titleView) + + let navigationFrameStart = sourceMessageFrameStart.insetBy(dx: .zero, dy: -15) + // let contentViewFrame = originalProjectedContentViewFrame.1 + let navigationFrameEnd = self.view.convert(controller.controller.navigationBar!.frame, from: controller.controller.navigationBar!.view) + + let convertedFrame = self.view + .convert(titleView.frame, from: titleView) // CGRect(x: (contentViewFrame.width - titleSourceframe.width) / 2, y: contentViewFrame.minY + titleSourceframe.height / 2, width: titleSourceframe.width, height: titleSourceframe.height) + let titleFinalFrame = CGRect( + x: (originalProjectedContentViewFrame.1.width - convertedFrame.width) / 2, + y: convertedFrame.minY - (titleView.frame.height - convertedFrame.height.rounded()) * 2 - 5, + width: convertedFrame.width, + height: titleView.frame.height + ) + + // let heightScale = titleSourceframe.height / titleFinalFrame.height + + let targetAvatarSize = CGSize(width: 36, height: 36) + let avatarFinalFrame = CGRect( + x: titleFinalFrame.minX - targetAvatarSize.width / 2, + y: titleFinalFrame.midY - targetAvatarSize.height / 2, + width: targetAvatarSize.width, + height: targetAvatarSize.height + ) + + let smallAvatarHeight = CGFloat(20) + + let sourceMessageFrameFinal = CGRect( + x: avatarFinalFrame.minX, + y: avatarFinalFrame.midY - sourceMessageFrameStart.height / 2, + width: sourceMessageFrameStart.width, + height: sourceMessageFrameStart.height + ) + + let startedBackgrounMaskPath = UIBezierPath(roundedRect: sourceMessageFrameStart, cornerRadius: .zero) + let targtePath = UIBezierPath(roundedRect: contentArea, cornerRadius: 40) + + let targetBackgroundMaskLayer = CAShapeLayer() + targetBackgroundMaskLayer.frame = clippingNode.layer.bounds + targetBackgroundMaskLayer.position = targeBgPosition + navigationBackgroundSnapshot.mask = targetBackgroundMaskLayer + navigationBackgroundSnapshot.masksToBounds = true + + targetBackgroundMaskLayer.path = targtePath.cgPath + targetBackgroundMaskLayer.path = targtePath.cgPath + clippingNode.layer.mask = targetBackgroundMaskLayer + let animation = clippingNode.layer.makeAnimation(from: startedBackgrounMaskPath.cgPath, to: targtePath.cgPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) { _ in + print("mask path updated") + } + animation.fillMode = .forwards + targetBackgroundMaskLayer.add(animation, forKey: "path") + + print("titleSourceframe: \(titleSourceframe) titleFinalFrame: \(titleFinalFrame) navigationFrameStar: \(navigationFrameStart)") + + avatarSnapshot.layer.animateScale(from: 1.0, to: smallAvatarHeight / avatarFinalFrame.height, duration: 1.0) + + chatSnapshot.frame = sourceMessageFrameStart + chatSnapshot.layer.addSublayer(navigationBackgroundSnapshot) + self.view.addSubview(chatSnapshot) + chatSnapshot.layer.animatePosition(from: sourceMessageFrameStart.center, to: sourceMessageFrameFinal.center, duration: 0.3, removeOnCompletion: true) { _ in + print("title snapshot animation finished") + chatSnapshot.removeFromSuperview() + } + chatSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) + + titleSnapshot.frame = titleSourceframe + self.view.addSubview(titleSnapshot) + titleSnapshot.layer.animatePosition(from: titleSourceframe.center, to: titleFinalFrame.center, duration: 0.3, removeOnCompletion: true) { _ in + print("title snapshot animation finished") + chatSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 1.5, removeOnCompletion: false) + titleSnapshot.removeFromSuperview() + } + + + avatarSnapshot.frame = avatarSourceFrame + self.view.addSubview(avatarSnapshot) + avatarSnapshot.layer.animatePosition(from: avatarSourceFrame.center, to: avatarFinalFrame.center, duration: 0.3, removeOnCompletion: true) { _ in + print("avatarSnapshot snapshot animation finished") + avatarSnapshot.removeFromSuperview() + } + avatarSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) + + + let navigationSnapshotView = UIView() + navigationSnapshotView.layer.addSublayer(navigationBackgroundSnapshot) + navigationSnapshotView.frame = navigationFrameStart + navigationSnapshotView.layer.animatePosition(from: navigationFrameStart.center, to: navigationFrameEnd.center, duration: 0.3, removeOnCompletion: true) { _ in + print("navigationSnapshotView snapshot animation finished") + navigationSnapshotView.removeFromSuperview() + } + + let pf = originalProjectedContentViewFrame.1 + + CATransaction.commit() + +// self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: targetBackgroundMaskLay cgPath), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in +// self?.animatedIn = true +// }) + } + } + + func updateLayout() { + if let layout = self.validLayout { + self.updateLayout(layout, transition: .immediate) + } + } + + func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.isAnimatingOut { + return + } + + self.validLayout = layout + + let targetFrame = CGRect(origin: CGPoint(), size: layout.size) + transition.updateFrame(view: self.effectView, frame: targetFrame) + transition.updateFrame(node: self.dimNode, frame: targetFrame) + transition.updateFrame(node: self.withoutBlurDimNode, frame: targetFrame) + + switch layout.metrics.widthClass { + case .compact: + if self.effectView.superview == nil { + self.view.insertSubview(self.effectView, at: 0) + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) + self.dimNode.alpha = 1.0 + } + self.dimNode.isHidden = false + self.withoutBlurDimNode.isHidden = true + case .regular: + if self.effectView.superview != nil { + self.effectView.removeFromSuperview() + self.withoutBlurDimNode.alpha = 1.0 + } + self.dimNode.isHidden = true + self.withoutBlurDimNode.isHidden = false + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let actionsSideInset: CGFloat = layout.safeInsets.left + 11 + let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) + + let actionsBottomInset: CGFloat = 11.0 + + if let contentParentNode = contentContainerNode.controllerNode { + var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view) + if let presentationArguments, let (sourceNode, sourceRect) = presentationArguments.sourceNodeAndRect() { + projectedFrame = convertFrame(sourceRect, from: sourceNode.view, to: self.view) + } + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentArea = self.presentationArguments?.contentArea() { + let topEdge = max(contentTopInset, contentArea.minY) + + let constrainedWidth: CGFloat + if layout.size.width < layout.size.height { + constrainedWidth = layout.size.width + } else { + constrainedWidth = floor(layout.size.width / 2.0) + } + let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth + + var contentUnscaledSize: CGSize + if case .compact = layout.metrics.widthClass { + let proposedContentHeight: CGFloat + if layout.size.width < layout.size.height { + proposedContentHeight = layout.size.height - topEdge - actionsSideInset - layout.intrinsicInsets.bottom - actionsBottomInset + } else { + proposedContentHeight = layout.size.height - topEdge - topEdge + } + + contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) + if + let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout( + size: contentUnscaledSize, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + deviceMetrics: layout.deviceMetrics, + intrinsicInsets: UIEdgeInsets(), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + )) { + contentUnscaledSize = preferredSize + } + } else { + let proposedContentHeight = layout.size.height - topEdge - actionsSideInset - layout.intrinsicInsets.bottom + + contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(400.0, proposedContentHeight)) + if + let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout( + size: contentUnscaledSize, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + deviceMetrics: layout.deviceMetrics, + intrinsicInsets: UIEdgeInsets(), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + )) { + contentUnscaledSize = preferredSize + } + } + let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) + self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) + + let contentActionsSpacing: CGFloat = .zero + let actionsSize: CGSize = .zero + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + var originalActionsFrame: CGRect + var originalContentFrame: CGRect + var contentHeight: CGFloat + + if case .compact = layout.metrics.widthClass { + if layout.size.width < layout.size.height { + let sideInset = floor((layout.size.width - contentSize.width) / 2.0) + originalActionsFrame = CGRect(origin: CGPoint(x: sideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - actionsSideInset - contentSize.height) / 2.0) + contentSize.height)), size: actionsSize) + originalContentFrame = CGRect(origin: CGPoint(x: sideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + } else { + originalContentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width - actionsSideInset - actionsSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + actionsSideInset, y: max(topEdge, originalContentFrame.minY)), size: actionsSize) + contentHeight = max(layout.size.height, max(originalContentFrame.maxY, originalActionsFrame.maxY)) + } + } else { + originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) + originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width) + originalContentFrame.origin.x = max(originalContentFrame.origin.x, actionsSideInset) + originalContentFrame.origin.y = min(originalContentFrame.origin.y, layout.size.height - layout.intrinsicInsets.bottom - actionsSideInset - contentSize.height) + originalContentFrame.origin.y = max(originalContentFrame.origin.y, contentTopInset) + if originalContentFrame.maxX <= layout.size.width - actionsSideInset - actionsSize.width - contentActionsSpacing { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + contentActionsSpacing, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.maxX > layout.size.width - actionsSideInset { + let offset = originalActionsFrame.maxX - (layout.size.width - actionsSideInset) + originalActionsFrame.origin.x -= offset + originalContentFrame.origin.x -= offset + } + } else { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentActionsSpacing - actionsSize.width, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.minX < actionsSideInset { + let offset = actionsSideInset - originalActionsFrame.minX + originalActionsFrame.origin.x += offset + originalContentFrame.origin.x += offset + } + } + contentHeight = layout.size.height + contentHeight = max(contentHeight, originalActionsFrame.maxY + actionsBottomInset) + contentHeight = max(contentHeight, originalContentFrame.maxY + actionsBottomInset) + } + + let scrollContentSize = CGSize(width: layout.size.width, height: contentSize.height) + print("originalContentFrame: \(originalContentFrame) contentTopInset: \(contentTopInset) scrollContentSize: \(scrollContentSize)") + + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if let maskLayer = self.clippingNode.layer.mask as? CAShapeLayer { + let newPath = UIBezierPath(roundedRect: originalContentFrame, cornerRadius: 40) + transition.updatePath(layer: maskLayer, path: newPath.cgPath) + } + +// let contentContainerFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) +// transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + } + } + + transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)) + self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) + } + + private func animateBlurBackground(isHidden: Bool) {} + + func animateOut(targetNode: ASDisplayNode?, completion: (() -> Void)? = nil) { + var dimCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + } + + func updatePresentationArguments(_ arguments: ChatListPreviewPresentationData?, controller: ViewController?) { + self.presentationArguments = arguments + self.controller = controller +// guard +// let (sourceNode, sourceFrame) = arguments?.sourceNodeAndRect(), +// let contentArea = arguments?.contentArea(), +// let sourceChatItem = sourceNode.supernode as? ChatListItemNode, +// let controller = controller +// else { return } +// +// self.gesture?.endPressedAppearance() +// +// let titleNode = sourceChatItem.titleNode +// let titleSnapshot = titleNode.layer.snapshotContentTree() +// let titleSourceframe = titleNode.convert(titleNode.frame, to: nil) +// +// let avatarNode = sourceChatItem.avatarContainerNode +// let avatarSnapshot = avatarNode.layer.snapshotContentTree() +// let avatarSourceFrame = avatarNode.convert(avatarNode.frame, to: nil) +// +// titleNode.isHidden = true +// avatarNode.isHidden = true +// +// guard +// let chatSnapshot = sourceChatItem.layer.snapshotContentTree(), +// let sourceMessageNode, +// let titleSnapshot, +// let avatarSnapshot, +// let navigationBar = controller.navigationBar, +// let avatarNode = navigationBar.subnodes?.compactMap({ $0.subnodes }).flatMap({ $0 }).first(where: { $0 is NavigationButtonNode }) as? NavigationButtonNode, +// let titleView = controller.navigationItem.titleView as? ChatTitleView, +// let navigationBackgroundSnapshot = navigationBar.layer.snapshotContentTree() +// else { return } +// +// let sourceMessageFrameStart = sourceNode.convert(sourceFrame, to: nil)//.align(in: finalFrame) +// +// let finalFrame = previewFrame(from: contentArea) +// let navigationFrameStart = sourceMessageFrameStart.insetBy(dx: .zero, dy: -15) +// let navigationFrameEnd = CGRect(origin: finalFrame.origin, size: CGSize(width: finalFrame.width, height: 43.0)) +// +// let titleFinalFrame = CGRect(x: (finalFrame.width - titleSourceframe.width) / 2, y: finalFrame.minX + titleSourceframe.height / 2, width: titleSourceframe.width, height: titleSourceframe.height) +// let targetAvatarSize = CGSize(width: 36, height: 36) +// let avatarFinalFrame = CGRect(x: (titleFinalFrame.minX - targetAvatarSize.width) / 2 + finalFrame.minX, +// y: (navigationFrameEnd.height - targetAvatarSize.height) / 2, width: targetAvatarSize.width, height: targetAvatarSize.height) +// +// let targetAvatarStart = CGRect(origin: CGPoint(x: finalFrame.maxX - 5 - (targetAvatarSize.width / 2), +// y: (sourceMessageFrameStart.minY - (targetAvatarSize.height / 2))), +// size: CGSize(width: targetAvatarSize.width / 2, height: targetAvatarSize.height / 2)) +// +// let contentScale = self.view.contentScaleFactor +// +// chatSnapshot.bounds = sourceMessageFrameStart +// chatSnapshot.contentsScale = contentScale +// chatSnapshot.contentsGravity = .resizeAspect +// chatSnapshot.position = sourceMessageFrameStart.center +// sourceMessageNode.layer.addSublayer(chatSnapshot) +// +// let targetBackgroundMaskLayer = CAShapeLayer() +// targetBackgroundMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: navigationFrameStart.size), cornerRadius: .zero).cgPath +// targetBackgroundMaskLayer.bounds = sourceMessageFrameStart +// targetBackgroundMaskLayer.position = sourceMessageFrameStart.center +// navigationBackgroundSnapshot.mask = targetBackgroundMaskLayer +// sourceMessageNode.layer.addSublayer(navigationBackgroundSnapshot) +// +// titleSnapshot.bounds = titleSourceframe +// titleSnapshot.position = titleSourceframe.center +// titleSnapshot.contentsScale = contentScale +// titleSnapshot.contentsGravity = .resizeAspect +// sourceMessageNode.layer.addSublayer(titleSnapshot) +// +// self.sourceAvatarNode?.bounds = avatarSourceFrame +// self.sourceAvatarNode?.position = avatarSourceFrame.center +// self.sourceAvatarNode?.layer.addSublayer(avatarSnapshot) +// avatarSnapshot.contentsScale = contentScale +// avatarSnapshot.contentsGravity = .resizeAspect +// avatarSnapshot.frame = self.sourceAvatarNode!.layer.bounds +// +// let sourceMessageFrameEnd = CGRect(x: finalFrame.minX + 60, y: navigationFrameEnd.minY, width: sourceMessageFrameStart.width, height: sourceMessageFrameStart.height) +// let targtetAvatarEnd = CGRect(origin: CGPoint(x: finalFrame.maxX - 5 - targetAvatarSize.width, y: finalFrame.minY + 5), size: targetAvatarSize) +// +// +// self.transitionParams = TransitionParams(contentArea: finalFrame, +// sourceMessageFrameStart: sourceMessageFrameStart, +// sourceMessageFrameEnd: sourceMessageFrameEnd, +// sourceChatItemSnapshot: chatSnapshot, +// sourceTitleSnapshot: titleSnapshot, +// sourceTitleFrame: titleSourceframe, + //// sourceAvatarSnapshot: avatarSnapshot, +// sourceAvatarStartFrame: avatarSourceFrame, +// sourceAvatarFinalFrame: avatarFinalFrame, +// targetTitleView: titleView, +// targetTitleViewFrame: titleFinalFrame, +// targetAvatarNode: avatarNode, +// targetAvatarFrameStart: targetAvatarStart, +// targetAvatarFrameEnd: targtetAvatarEnd, +// targetBackgroundLayer: navigationBackgroundSnapshot, +// targetBackgroundMaskLayer: targetBackgroundMaskLayer, +// targetBackgroundStartFrame: navigationFrameStart, +// targetBackgroundEndFrame: navigationFrameEnd) +// +// print("calculated transition params: \(self.transitionParams!)") + } + + private func previewFrame(from contentArea: CGRect) -> CGRect { + let size = CGSize(width: min(self.bounds.width - 22, contentArea.width), height: min(400, contentArea.height)) + return CGRect(x: (self.bounds.width - size.width) / 2, y: (self.bounds.height - size.height) / 2, width: size.width, height: size.height) + } +} + +public final class ChatListPreviewController: ViewController { + private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? + private weak var gesture: ContextGesture? + + private var animatedDidAppear = false + private var wasDismissed = false + + private var controllerNode: ChatListPreviewControllerNode { + return self.displayNode as! ChatListPreviewControllerNode + } + + private var animatedIn = false + + private let context: AccountContext + private var chatLocation: ChatLocation + private var chatPrevewController: ChatController + + public init(context: AccountContext, chatLocation: ChatLocation, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) { + self.context = context + self.chatLocation = chatLocation + self.chatPrevewController = context.sharedContext.makeChatController(context: context, chatLocation: chatLocation, subject: nil, botStart: nil, mode: .standard(previewing: true)) + self.recognizer = recognizer + self.gesture = gesture + super.init(navigationBarPresentationData: nil) + self.statusBar.statusBarStyle = .Hide + self.lockOrientation = true + self.blocksBackgroundWhenInOverlay = true + } + + @available(*, unavailable) public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = ChatListPreviewControllerNode(context: context, gesture: gesture) + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + self.displayNodeDidLoad() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let _ = self.presentationArguments as? ChatListPreviewPresentationData { + self.updateChatLocation(self.chatLocation) + self.ready.set(.single(true)) + } + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + _ = (self.ready.get() |> deliverOnMainQueue).start(next: { value in + guard value else { return } + + self.controllerNode.initializeContent() + + print("content is ready: \(value)") + if !self.wasDismissed, !self.animatedDidAppear { + self.animatedDidAppear = true + self.controllerNode.animateIn() + } + }) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + guard + let arguments = self.presentationArguments as? ChatListPreviewPresentationData, + let (sourceNode, _) = arguments.sourceNodeAndRect() + else { return } + + self.controllerNode.animateOut(targetNode: sourceNode, completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + self.chatPrevewController.updateNavigationBarLayout(layout, transition: transition) + self.controllerNode.updateLayout(layout, transition: transition) + } + + public func updateChatLocation(_ chatLocation: ChatLocation) { + self.chatLocation = chatLocation + let chatController = context.sharedContext.makeChatController(context: self.context, chatLocation: chatLocation, subject: nil, botStart: nil, mode: .standard(previewing: true)) + self.chatPrevewController = chatController + if let layout = self.currentlyAppliedLayout { + chatController.updateNavigationBarLayout(layout, transition: .immediate) + self.controllerNode.updatePresentationArguments( + self.presentationArguments as? ChatListPreviewPresentationData, + controller: chatController + ) + } + +// if self.chatLocation != chatLocation { +// } + } +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 8dc2cd66fa9..8256c39c33c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -850,7 +850,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { hasUnseenCloseFriends: stats.hasUnseenCloseFriends ) } - )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, params: .emptyVisibleParams, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { @@ -3591,7 +3591,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { topForumTopicItems: [], autoremoveTimeout: nil, storyState: nil - )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, params: .emptyVisibleParams, interaction: interaction) case .media: return nil case .links: diff --git a/submodules/ChatListUI/Sources/Node/ChatListArchiveTransitionItem.swift b/submodules/ChatListUI/Sources/Node/ChatListArchiveTransitionItem.swift new file mode 100644 index 00000000000..e38e47fcfdb --- /dev/null +++ b/submodules/ChatListUI/Sources/Node/ChatListArchiveTransitionItem.swift @@ -0,0 +1,627 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import AvatarNode +import SwiftSignalKit +import AnimationUI +import ComponentFlow +import TelegramPresentationData + +public struct ArchiveAnimationParams: Equatable { + public let scrollOffset: CGFloat + public let storiesFraction: CGFloat + public private(set)var expandedHeight: CGFloat + public private(set)var isRevealed: Bool = false + public let finalizeAnimation: Bool + public private(set)var isHiddenByDefault: Bool + + public static var emptyVisibleParams: ArchiveAnimationParams { + return ArchiveAnimationParams(scrollOffset: .zero, storiesFraction: .zero, expandedHeight: .zero, finalizeAnimation: false, isHiddenByDefault: false) + } + + public static var emptyDefaultHiddenParams: ArchiveAnimationParams { + return ArchiveAnimationParams(scrollOffset: .zero, storiesFraction: .zero, expandedHeight: .zero, finalizeAnimation: false, isHiddenByDefault: true) + } + +// public init(scrollOffset: CGFloat, storiesFraction: CGFloat, expandedHeight: CGFloat, finalizeAnimation: Bool, isHiddenByDefault: Bool) { +// +// } + + public func withUpdatedFinalizeAnimation(_ finalizeAnimation: Bool) -> ArchiveAnimationParams { + var newParams = ArchiveAnimationParams( + scrollOffset: self.scrollOffset, + storiesFraction: self.storiesFraction, + expandedHeight: self.expandedHeight, + finalizeAnimation: finalizeAnimation, + isHiddenByDefault: self.isHiddenByDefault + ) + if finalizeAnimation { + if newParams.isArchiveGroupVisible { + newParams.expandedHeight /= 1.2 + newParams.isRevealed = true + } else { + newParams = ArchiveAnimationParams( + scrollOffset: .zero, + storiesFraction: .zero, + expandedHeight: .zero, + finalizeAnimation: finalizeAnimation, + isHiddenByDefault: self.isHiddenByDefault + ) + newParams.isRevealed = false + } + } + return newParams + } + + public mutating func updateVisibility(isRevealed: Bool? = nil, isHiddenByDefault: Bool? = nil) { + if let isRevealed { + self.isRevealed = isRevealed + } + if let isHiddenByDefault { + self.isHiddenByDefault = isHiddenByDefault + } + } + + var isArchiveGroupVisible: Bool { + return (storiesFraction >= 0.85 && finalizeAnimation) || !isHiddenByDefault + } + +} + +class ChatListArchiveTransitionNode: ASDisplayNode { + let backgroundNode: ASDisplayNode + let gradientContainerNode: ASDisplayNode + let gradientImageNode: ASImageNode + let topShadowNode: ASImageNode + let bottomShadowNode: ASImageNode + let titleNode: ASTextNode //centered + let arrowBackgroundNode: ASDisplayNode //20 with insets 10 + let arrowContainerNode: ASDisplayNode + let arrowAnimationNode: AnimationNode //20x20 + let arrowImageNode: ASImageNode + var arrowSwipeDownIcon: UIImage? + var arrowReleaseBackgroundColor: UIColor? + var arrowReleaseIcon: UIImage? + + var animation: TransitionAnimation + var presentationData: ChatListPresentationData? + var hapticFeedback: HapticFeedback? + + required override init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = .clear + self.backgroundNode.isLayerBacked = true + + self.animation = .init(state: .swipeDownInit, params: .emptyDefaultHiddenParams) + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + + self.gradientContainerNode = ASDisplayNode() + self.gradientContainerNode.isLayerBacked = true + self.gradientImageNode = ASImageNode() + self.gradientImageNode.isLayerBacked = true + + self.topShadowNode = ASImageNode() + self.topShadowNode.isLayerBacked = true + self.topShadowNode.displayWithoutProcessing = true + self.topShadowNode.alpha = 0.5 + + self.bottomShadowNode = ASImageNode() + self.bottomShadowNode.isLayerBacked = true + self.bottomShadowNode.displayWithoutProcessing = true + self.bottomShadowNode.alpha = 0.5 + + self.arrowBackgroundNode = ASDisplayNode() + self.arrowBackgroundNode.backgroundColor = .white.withAlphaComponent(0.4) + self.arrowBackgroundNode.isLayerBacked = true + + self.arrowContainerNode = ASDisplayNode() + self.arrowContainerNode.isLayerBacked = true + + self.arrowImageNode = ASImageNode() + self.arrowImageNode.isLayerBacked = true + + self.arrowAnimationNode = AnimationNode(animation: "anim_arrow_to_archive", scale: 0.33) + self.arrowAnimationNode.backgroundColor = .clear + self.arrowAnimationNode.isHidden = true + + super.init() + self.addSubnode(self.gradientContainerNode) + self.gradientContainerNode.addSubnode(self.gradientImageNode) + self.gradientContainerNode.addSubnode(self.topShadowNode) + self.gradientContainerNode.addSubnode(self.bottomShadowNode) + self.addSubnode(self.backgroundNode) + self.backgroundNode.addSubnode(self.titleNode) + self.backgroundNode.addSubnode(self.arrowBackgroundNode) + self.backgroundNode.addSubnode(self.arrowContainerNode) + self.arrowContainerNode.addSubnode(self.arrowImageNode) + self.addSubnode(self.arrowAnimationNode) + } + + override func didLoad() { + super.didLoad() + self.arrowBackgroundNode.layer.cornerRadius = 11 + self.arrowBackgroundNode.layer.masksToBounds = true + } + + override func layout() { + super.layout() + guard let theme = presentationData?.theme else { return } + print("bounds: \(self.bounds)") + let gradientImageSize = self.bounds.size + let greyColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors + if self.gradientImageNode.image == nil { + let greyGradientImage = generateGradientImage(size: gradientImageSize, colors: [greyColors.0, greyColors.1], locations: [1.0, 0.0], direction: .horizontal) + self.gradientImageNode.image = greyGradientImage + } + + let blueColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors + if self.animation.gradientImage == nil { + let blueGradientImage = generateGradientImage(size: gradientImageSize, colors: [blueColors.0, blueColors.1], locations: [1.0, 0.0], direction: .horizontal) + self.animation.gradientImage = blueGradientImage + } + + if self.animation.rotatedGradientImage == nil { + let blueRotatedGradientImage = generateGradientImage(size: gradientImageSize, colors: [blueColors.1, blueColors.1, blueColors.0, blueColors.0], locations: [1.0, 0.65, 0.15, 0.0], direction: .vertical) + self.animation.rotatedGradientImage = blueRotatedGradientImage + } + + let greyGradientColorAtFraction = greyColors.0.interpolateTo(greyColors.1, fraction: 40 / gradientImageSize.width) + if let greyGradientColorAtFraction, self.arrowSwipeDownIcon == nil { + self.arrowSwipeDownIcon = PresentationResourcesItemList.archiveTransitionArrowIcon(theme, backgroundColor: greyGradientColorAtFraction) + } + + let blueGradientColorAtFraction = blueColors.0.interpolateTo(blueColors.1, fraction: 40 / gradientImageSize.width) + if let blueGradientColorAtFraction { + if self.arrowReleaseIcon == nil { + self.arrowReleaseIcon = PresentationResourcesItemList.archiveTransitionArrowIcon(theme, backgroundColor: blueGradientColorAtFraction) + } + if self.arrowReleaseBackgroundColor == nil { + arrowAnimationNode.setAnimation(name: "anim_arrow_to_archive", colors: [ + "Arrow 1.Arrow 1.Stroke 1": blueGradientColorAtFraction, + "Arrow 2.Arrow 2.Stroke 1": blueGradientColorAtFraction, + "Cap.cap2.Fill 1": .white, + "Cap.cap1.Fill 1": .white, + "Box.box1.Fill 1": .white + ]) + self.arrowReleaseBackgroundColor = blueGradientColorAtFraction + } + } + if self.topShadowNode.image == nil { + let shadowGradient = generateGradientImage(size: CGSize(width: gradientImageSize.width, height: 20), colors: [.black.withAlphaComponent(0.1), .black.withAlphaComponent(0.0)], locations: [0.0, 1.0], direction: .vertical) + self.topShadowNode.image = shadowGradient + self.bottomShadowNode.image = shadowGradient + } + } + + func updateLayout(transition: ContainedViewLayoutTransition, size: CGSize, params: ArchiveAnimationParams, presentationData: ChatListPresentationData, avatarNode: AvatarNode) { + let frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width, height: self.bounds.height)) + + guard !(self.animation.params.finalizeAnimation && params.finalizeAnimation) else { + return + } + + guard self.animation.params != params || self.frame.size != size else { return } + + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + + let updateLayers = self.animation.params != params + + self.animation.params = params +// print("params: \(params) \nprevious params: \(self.animation.params) \nsize: \(size) previous size: \(self.frame.size)") + let previousState = self.animation.state + self.animation.state = .init(params: params, previousState: previousState) + self.animation.presentationData = presentationData + if self.presentationData?.theme != presentationData.theme { + print("need to update gradients") + } + self.presentationData = presentationData + +// if updateLayers { +// transition = .animated(duration: 1.0, curve: .easeInOut) +// } + + transition.updatePosition(node: self.backgroundNode, position: frame.center) + transition.updateBounds(node: self.backgroundNode, bounds: frame) + + transition.updatePosition(node: self.gradientContainerNode, position: frame.center) + transition.updateBounds(node: self.gradientContainerNode, bounds: frame) + + transition.updatePosition(node: self.gradientImageNode, position: frame.center) + transition.updateBounds(node: self.gradientImageNode, bounds: frame) + + + let difference = (frame.height - params.expandedHeight).rounded() + + let topShadowFrame = CGRect(x: .zero, y: difference - 10, width: frame.width, height: 20) + transition.updatePosition(node: self.topShadowNode, position: topShadowFrame.center, beginWithCurrentState: true) + transition.updateBounds(node: self.topShadowNode, bounds: topShadowFrame, force: true, beginWithCurrentState: true) + + let bottomShadowFrame = CGRect(x: .zero, y: frame.height - 10, width: frame.width, height: 20) + transition.updateTransformRotation(node: self.bottomShadowNode, angle: TransitionAnimation.degreesToRadians(180)) + transition.updatePosition(node: self.bottomShadowNode, position: bottomShadowFrame.center, beginWithCurrentState: true) + transition.updateBounds(node: self.bottomShadowNode, bounds: bottomShadowFrame, force: true, beginWithCurrentState: true) + + let arrowBackgroundHeight = max(0, (frame.height - difference - 22)) + let arrowBackgroundFrame = CGRect(x: 29, y: frame.height - arrowBackgroundHeight - 11, width: 22, height: arrowBackgroundHeight) + let arrowFrame = CGRect(x: arrowBackgroundFrame.minX, y: arrowBackgroundFrame.maxY - 22, width: 22, height: 22) + if self.arrowBackgroundNode.position == .zero || self.arrowBackgroundNode.bounds.height == .zero { + self.arrowBackgroundNode.position = arrowBackgroundFrame.center + self.arrowBackgroundNode.bounds = arrowBackgroundFrame + } + + transition.updatePosition(node: self.arrowBackgroundNode, position: arrowBackgroundFrame.center, beginWithCurrentState: true) + transition.updateBounds(node: self.arrowBackgroundNode, bounds: arrowBackgroundFrame, force: true, beginWithCurrentState: true) + + transition.updatePosition(node: self.arrowContainerNode, position: arrowFrame.center) + transition.updateBounds(node: self.arrowContainerNode, bounds: arrowFrame) + switch self.animation.state { + case .swipeDownInit, .swipeDownAppear, .swipeDownDidAppear: + self.arrowImageNode.image = self.arrowSwipeDownIcon + case .releaseDidAppear, .releaseAppear: + self.arrowImageNode.image = self.arrowReleaseIcon + } + transition.updatePosition(node: self.arrowImageNode, position: arrowFrame.center) + transition.updateBounds(node: self.arrowImageNode, bounds: arrowFrame) + + if let size = self.arrowAnimationNode.preferredSize(), !params.finalizeAnimation { + let arrowAnimationFrame = CGRect(x: arrowFrame.midX - size.width / 2, y: arrowFrame.midY - size.height / 2, width: size.width, height: size.height) + transition.updatePosition(node: arrowAnimationNode, position: arrowAnimationFrame.center) + transition.updateBounds(node: arrowAnimationNode, bounds: arrowAnimationFrame) + } + + if self.titleNode.attributedText == nil { + self.titleNode.attributedText = NSAttributedString(string: "Swipe down for archive", attributes: [ + .foregroundColor: UIColor.white, + .font: Font.medium(floor(presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) + ]) + } + + if updateLayers { + let nodesToHide: [ASDisplayNode] = [self.gradientImageNode, self.backgroundNode] + nodesToHide.filter({ $0.isHidden }).forEach({ $0.isHidden = false }) + + if animation.state == .releaseAppear { + self.hapticFeedback?.impact(.medium) + } else if animation.state == .swipeDownAppear && previousState == .releaseDidAppear { + self.hapticFeedback?.impact(.medium) + } + + self.animation.animateLayers(gradientNode: self.gradientContainerNode, + textNode: self.titleNode, + arrowContainerNode: self.arrowContainerNode, + arrowAnimationNode: self.arrowAnimationNode, + avatarNode: avatarNode, + transition: transition, finalizeCompletion: { [weak self] isFinished in + guard let self else { return } + if !isFinished { + self.hapticFeedback?.impact(.medium) + nodesToHide.forEach({ $0.isHidden = true }) + } + }) + } + } + + + + struct TransitionAnimation { + enum TextPosition { + case centered + case left + case right + } + + enum State: Int { + case swipeDownInit + case releaseAppear + case releaseDidAppear + case swipeDownAppear + case swipeDownDidAppear + + init(params: ArchiveAnimationParams, previousState: TransitionAnimation.State) { + let fraction = params.storiesFraction + if params.storiesFraction < 0.85 { + switch previousState { + case .swipeDownAppear, .swipeDownInit, .swipeDownDidAppear: + self = .swipeDownDidAppear + default: + self = .swipeDownAppear + } + } else if fraction >= 0.85 && fraction <= 1.0 { + switch previousState { + case .releaseAppear, .releaseDidAppear: + self = .releaseDidAppear + default: + self = .releaseAppear + } + } else { + self = .swipeDownInit + } + } + + func animationProgress(fraction: CGFloat) -> CGFloat { + switch self { + case .swipeDownAppear: + return max(0.01, min(0.99, fraction / 0.85)) + case .releaseAppear: + return max(0.01, min(0.99, (fraction - 0.85) / 0.15)) + default: + return 1.0 + } + } + } + + var state: State + var params: ArchiveAnimationParams + var presentationData: ChatListPresentationData? + + var isAnimated = false + var gradientMaskLayer: CAShapeLayer? + var gradientLayer: CALayer? + var releaseTextNode: ASTextNode? + + var gradientImage: UIImage? { + didSet { + if let gradientLayer, + gradientLayer.contents == nil { + gradientLayer.contents = self.gradientImage + } + } + } + var rotatedGradientImage: UIImage? + + static func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + static func distance(from: CGPoint, to point: CGPoint) -> CGFloat { + return sqrt(pow((point.x - from.x), 2) + pow((point.y - from.y), 2)) + } + + mutating func animateLayers( + gradientNode: ASDisplayNode, + textNode: ASTextNode, + arrowContainerNode: ASDisplayNode, + arrowAnimationNode: AnimationNode, + avatarNode: AvatarNode, + transition: ContainedViewLayoutTransition, + finalizeCompletion: ((Bool) -> Void)? + ) { + print(""" + animate layers with fraction: \(self.params.storiesFraction) animation progress: \(self.state.animationProgress(fraction: self.params.storiesFraction)) + state: \(self.state), offset: \(self.params.scrollOffset) height: \(self.params.expandedHeight) + ## + """) + + if !arrowAnimationNode.isHidden, state != .releaseDidAppear { + arrowAnimationNode.isHidden = true + } + + switch state { + case .releaseAppear: + updateReleaseTextNode(from: textNode) + makeGradientOverlay(gradientContainerNode: gradientNode, arrowContainerNode: arrowContainerNode) +// let animationProgress = self.state.animationProgress(fraction: self.params.storiesFraction) + + if let releaseTextNode { transition.updateAlpha(node: releaseTextNode, alpha: 1.0) } + self.animateTextNodePositionIfNeeded(textNode: releaseTextNode, targetTextPosition: .centered, transition: transition, needShake: true) + + if let gradientMaskLayer, let gradientLayer { + transition.updateAlpha(layer: gradientLayer, alpha: 1.0) + let targetPath = generateGradientMaskPath(gradientContainerNode: gradientNode, arrowContainerNode: arrowContainerNode, fraction: 1.0) + transition.updatePath(layer: gradientMaskLayer, path: targetPath.cgPath) + } + + transition.updateTransformRotation(node: arrowContainerNode, angle: TransitionAnimation.degreesToRadians(-180)) + transition.updateTransformRotation(node: arrowContainerNode, angle: TransitionAnimation.degreesToRadians(180)) + self.animateTextNodePositionIfNeeded(textNode: textNode, targetTextPosition: .right, transition: transition) + transition.updateAlpha(node: textNode, alpha: .zero) + case .releaseDidAppear: + if params.finalizeAnimation, gradientLayer?.superlayer != nil { + print("should finalize animation") + //duration = 0.5 + //show animation arrow node + //play animation arrow to archive + //update gradient mask path to avatar node frame + //scale up then scale down avatar node gradient + finalizeCompletion?(false) + arrowAnimationNode.isHidden = false + + let avatarNodeFrame = gradientNode.convert(avatarNode.contentNode.layer.frame, from: avatarNode.contentNode) + let avatarContentTranform = avatarNode.contentNode.layer.affineTransform() + + arrowAnimationNode.completion = { //[weak gradientLayer] in + print("arrow animation node finish animation") +// guard let gradientLayer else { return } + } + + avatarNode.transform = CATransform3DMakeAffineTransform(avatarContentTranform) + + transition.updatePosition(node: arrowAnimationNode, position: avatarNodeFrame.center) + transition.updateTransform(node: arrowAnimationNode, transform: avatarContentTranform, beginWithCurrentState: true) { _ in + transition.updateTransform(node: arrowAnimationNode, transform: avatarContentTranform.scaledBy(x: 1.0, y: 0.9)) { finished in + guard finished else { return } + transition.updateTransform(node: arrowAnimationNode, transform: avatarContentTranform.scaledBy(x: 0.9, y: 1.0)) { finished in + guard finished else { return } + transition.updateTransform(node: arrowAnimationNode, transform: avatarContentTranform) { _ in + arrowAnimationNode.isHidden = true + arrowAnimationNode.reset() + finalizeCompletion?(true) + } + } + } + } + arrowAnimationNode.play() + + + if let gradientMaskLayer, let gradientLayer { + gradientLayer.contents = rotatedGradientImage?.cgImage + let targetPath = UIBezierPath(roundedRect: avatarNodeFrame, cornerRadius: avatarNodeFrame.width / 2) + let scaledInset = avatarNodeFrame.width - avatarNodeFrame.width * 0.83 + let scaledAvatarNodeFrame = avatarNodeFrame.insetBy(dx: scaledInset, dy: scaledInset)//.applying(CGAffineTransform(scaleX: 0.83, y: 0.83)) + + let scaledTargetPath = UIBezierPath(roundedRect: scaledAvatarNodeFrame, cornerRadius: scaledAvatarNodeFrame.width / 2) + transition.updatePath(layer: gradientMaskLayer, path: scaledTargetPath.cgPath) { _ in + transition.updatePath(layer: gradientMaskLayer, path: targetPath.cgPath) + transition.updateTransform(node: avatarNode, transform: .identity) { _ in + transition.updateAlpha(layer: gradientLayer, alpha: .zero) { _ in + gradientLayer.removeFromSuperlayer() + gradientLayer.opacity = 1.0 + gradientLayer.contents = nil + } + } + } + } + print("avatar node frame: \(avatarNode.convert(avatarNode.frame, to: gradientNode))") + } else { + self.animateTextNodePositionIfNeeded(textNode: releaseTextNode, targetTextPosition: .centered, transition: transition) + } + case .swipeDownAppear, .swipeDownInit: +// let animationProgress: CGFloat = 0.0 + + transition.updateTransform(node: arrowContainerNode, transform: .identity) + transition.updateAlpha(node: textNode, alpha: 1.0) + + animateTextNodePositionIfNeeded(textNode: textNode, targetTextPosition: .centered, transition: transition, needShake: true) + self.animateTextNodePositionIfNeeded(textNode: releaseTextNode, targetTextPosition: .left, transition: transition) + + if let releaseTextNode { transition.updateAlpha(node: releaseTextNode, alpha: .zero) } + + if let gradientMaskLayer, let gradientLayer { + let targetPath = generateGradientMaskPath(gradientContainerNode: gradientNode, arrowContainerNode: arrowContainerNode, fraction: 0.02) + transition.updatePath(layer: gradientMaskLayer, path: targetPath.cgPath) + transition.updateAlpha(layer: gradientLayer, alpha: 0.6) + } + case .swipeDownDidAppear: + if !params.finalizeAnimation { + updateReleaseTextNode(from: textNode) + makeGradientOverlay(gradientContainerNode: gradientNode, arrowContainerNode: arrowContainerNode) + self.animateTextNodePositionIfNeeded(textNode: textNode, targetTextPosition: .centered, transition: transition) + } + } + self.isAnimated = true + } + + private func animateTextNodePositionIfNeeded(textNode: ASTextNode?, targetTextPosition: TextPosition, transition: ContainedViewLayoutTransition, needShake: Bool = false) { + guard let textNode, let supernode = textNode.supernode else { return } + + let textLayout = textNode.calculateLayoutThatFits(ASSizeRange( + min: .zero, + max: CGSize(width: supernode.bounds.width - 120, height: 25) + )) + + let targetX: CGFloat + switch targetTextPosition { + case .centered: + targetX = (supernode.bounds.width - textLayout.size.width) / 2 + case .left: + targetX = -textLayout.size.width + case .right: + targetX = supernode.bounds.width + } + + let targetFrame = CGRect( + x: targetX, + y: supernode.bounds.height - textLayout.size.height - 10, + width: textLayout.size.width, + height: textLayout.size.height + ) + + let positionDifference = textNode.position.x - targetFrame.center.x + + guard textNode.position != targetFrame.center || textNode.bounds != targetFrame else { return } + + transition.updateBounds(node: textNode, bounds: targetFrame, beginWithCurrentState: true) + transition.updatePosition(node: textNode, position: targetFrame.center, beginWithCurrentState: true) + + if needShake { + transition.updateTransform(node: textNode, transform: .init(translationX: positionDifference < 0 ? 10 : -10, y: .zero)) { _ in + transition.updateTransform(node: textNode, transform: .identity) + } + } + } + } +} + +extension ChatListArchiveTransitionNode.TransitionAnimation { + + private mutating func updateReleaseTextNode(from textNode: ASTextNode) { + if self.releaseTextNode == nil { + self.releaseTextNode = ASTextNode() + self.releaseTextNode?.isLayerBacked = true + guard let supernode = textNode.supernode, let releaseTextNode else { return } + + let attributes: [NSAttributedString.Key: Any] = textNode.attributedText?.attributes(at: 0, effectiveRange: nil) ?? [:] + releaseTextNode.attributedText = NSAttributedString(string: "Release for archive", attributes: attributes) + + let textLayout = textNode.calculateLayoutThatFits(ASSizeRange( + min: .zero, + max: CGSize(width: supernode.bounds.width - 120, height: 25) + )) + + releaseTextNode.frame = CGRect( + x: -textLayout.size.width, + y: supernode.bounds.height - textLayout.size.height - 10, + width: textLayout.size.width, + height: textLayout.size.height + ) + releaseTextNode.alpha = 0.0 + + supernode.addSubnode(self.releaseTextNode!) + } + } + + mutating internal func makeGradientOverlay(gradientContainerNode: ASDisplayNode, arrowContainerNode: ASDisplayNode) { + if self.gradientLayer == nil { + self.gradientLayer = CALayer() + self.gradientLayer?.contentsGravity = .resizeAspect + self.gradientLayer?.contentsScale = 3.0 + } + + if self.gradientMaskLayer == nil { + self.gradientMaskLayer = CAShapeLayer() + } + + guard let gradientLayer, let gradientMaskLayer else { return } + + if gradientLayer.superlayer == nil { + gradientContainerNode.layer.addSublayer(gradientLayer) + } + + if gradientMaskLayer.frame != gradientContainerNode.bounds { + gradientMaskLayer.frame = gradientContainerNode.bounds + gradientMaskLayer.path = generateGradientMaskPath(gradientContainerNode: gradientContainerNode, arrowContainerNode: arrowContainerNode, fraction: 0.02).cgPath + } + + if gradientLayer.frame != gradientContainerNode.bounds { + gradientLayer.frame = gradientContainerNode.bounds + gradientLayer.opacity = 0.6 + } + + if gradientLayer.mask == nil { + gradientLayer.mask = gradientMaskLayer + } + + gradientLayer.contents = self.gradientImage?.cgImage + } + + internal func generateGradientMaskPath(gradientContainerNode: ASDisplayNode, arrowContainerNode: ASDisplayNode, fraction: CGFloat) -> UIBezierPath { + let startRect = arrowContainerNode.convert(arrowContainerNode.bounds, to: gradientContainerNode) + let startRadius = startRect.width / 2 + + let finalScale = gradientContainerNode.bounds.width/startRect.width + gradientContainerNode.bounds.width/startRect.width*(gradientContainerNode.bounds.width - startRect.midX)/gradientContainerNode.bounds.width + let scale: CGFloat = (finalScale * fraction)//max(1.0, (finalScale * fraction)) + let scaleTransform = CGAffineTransform(scaleX: scale, y: scale) + var transformedRect = startRect.applying(scaleTransform) + let translation = CGPoint(x: startRect.center.x - transformedRect.center.x, y: startRect.center.y - transformedRect.center.y) + let translateTransform = CGAffineTransform(translationX: translation.x, y: translation.y) + let scaledRadius = startRadius * scale + transformedRect = transformedRect.applying(translateTransform) + + let path = UIBezierPath(roundedRect: transformedRect, cornerRadius: scaledRadius) + return path + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index bd7a1156a33..e8b416bcc8f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -188,6 +188,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { let selected: Bool let enableContextActions: Bool let hiddenOffset: Bool + var params: ArchiveAnimationParams let interaction: ChatListNodeInteraction public let selectable: Bool = true @@ -211,7 +212,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { } } - public init(presentationData: ChatListPresentationData, context: AccountContext, chatListLocation: ChatListControllerLocation, filterData: ChatListItemFilterData?, index: EngineChatList.Item.Index, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, header: ListViewItemHeader?, enableContextActions: Bool, hiddenOffset: Bool, interaction: ChatListNodeInteraction) { + public init(presentationData: ChatListPresentationData, context: AccountContext, chatListLocation: ChatListControllerLocation, filterData: ChatListItemFilterData?, index: EngineChatList.Item.Index, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, header: ListViewItemHeader?, enableContextActions: Bool, hiddenOffset: Bool, params: ArchiveAnimationParams, interaction: ChatListNodeInteraction) { self.presentationData = presentationData self.chatListLocation = chatListLocation self.filterData = filterData @@ -224,6 +225,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { self.header = header self.enableContextActions = enableContextActions self.hiddenOffset = hiddenOffset + self.params = params self.interaction = interaction } @@ -932,6 +934,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + let archiveTransitionNode: ChatListArchiveTransitionNode let contextContainer: ContextControllerSourceNode let mainContentContainerNode: ASDisplayNode @@ -950,7 +953,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var textArrowNode: ASImageNode? private var compoundTextButtonNode: HighlightTrackingButtonNode? let measureNode: TextNode - private var currentItemHeight: CGFloat? + var currentItemHeight: CGFloat? let forwardedIconNode: ASImageNode let textNode: TextNodeWithEntities var dustNode: InvisibleInkDustNode? @@ -1243,6 +1246,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true + self.archiveTransitionNode = ChatListArchiveTransitionNode() + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.isAccessibilityElement = true @@ -1251,6 +1256,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.separatorNode) self.addSubnode(self.contextContainer) + self.addSubnode(self.archiveTransitionNode) + self.contextContainer.addSubnode(self.mainContentContainerNode) self.avatarContainerNode.addSubnode(self.avatarNode) @@ -1453,7 +1460,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil - } + } strongSelf.updateVideoVisibility() } else { if let photo = peer.largeProfileImage, photo.hasVideo { @@ -2692,11 +2699,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0)) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) - var heightOffset: CGFloat = 0.0 - if item.hiddenOffset { - heightOffset = -itemHeight + var heightOffset: CGFloat = .zero + if case let .groupReference(data) = item.content, data.groupId == .archive, !item.params.isArchiveGroupVisible { + itemHeight *= 1.2 + heightOffset = -(itemHeight-item.params.expandedHeight) + print("height offset: \(heightOffset) with params: \(item.params) itemHeight: \(itemHeight)") } - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: max(0.0, itemHeight + heightOffset)), insets: insets) + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: itemHeight + heightOffset), insets: insets) var customActions: [ChatListItemAccessibilityCustomAction] = [] for option in peerLeftRevealOptions { @@ -2705,7 +2714,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { for option in peerRevealOptions { customActions.append(ChatListItemAccessibilityCustomAction(name: option.title, target: nil, selector: #selector(ChatListItemNode.performLocalAccessibilityCustomAction(_:)), key: option.key)) } - return (layout, { [weak self] synchronousLoads, animated in if let strongSelf = self { strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params, countersSize) @@ -2720,13 +2728,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { animateOnline = false } strongSelf.currentOnline = online - - if item.hiddenOffset { - strongSelf.layer.zPosition = -1.0 - } - - if case .groupReference = item.content { - strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, layout.contentSize.height - itemHeight, 0.0) + + if case let .groupReference(data) = item.content { + if data.groupId == .archive { + strongSelf.layer.zPosition = -1.0 + } + let translationY = layout.contentSize.height - itemHeight + strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, translationY, 0.0) +// print("set sublayer translation y: \(translationY)") } if let _ = updatedTheme { @@ -2743,10 +2752,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let contextContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: itemHeight)) -// strongSelf.contextContainer.position = contextContainerFrame.center transition.updatePosition(node: strongSelf.contextContainer, position: contextContainerFrame.center) transition.updateBounds(node: strongSelf.contextContainer, bounds: contextContainerFrame.offsetBy(dx: -strongSelf.revealOffset, dy: 0.0)) + var mainContentFrame: CGRect var mainContentBoundsOffset: CGFloat var mainContentAlpha: CGFloat = 1.0 @@ -2856,6 +2865,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.avatarNode.updateSize(size: avatarFrame.size) strongSelf.updateVideoVisibility() + if case let .groupReference(data) = item.content, data.groupId == .archive, item.params.isHiddenByDefault { + transition.updatePosition(node: strongSelf.archiveTransitionNode, position: contextContainerFrame.center) + transition.updateBounds(node: strongSelf.archiveTransitionNode, bounds: contextContainerFrame) + transition.updateAlpha(node: strongSelf.archiveTransitionNode, alpha: 1.0) + strongSelf.archiveTransitionNode.updateLayout( + transition: transition, + size: contextContainerFrame.size, + params: item.params, + presentationData: item.presentationData, + avatarNode: strongSelf.avatarNode + ) + } else { + transition.updateAlpha(node: strongSelf.archiveTransitionNode, alpha: .zero) + } + var itemPeerId: EnginePeer.Id? if case let .chatList(index) = item.index { itemPeerId = index.messageIndex.id.peerId @@ -3844,7 +3868,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { if case .groupReference = item.content { - self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, currentValue - (self.currentItemHeight ?? 0.0), 0.0) + let translationY = currentValue - (self.currentItemHeight ?? 0.0) + self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, translationY, 0.0) } else { var separatorFrame = self.separatorNode.frame separatorFrame.origin.y = currentValue - UIScreenPixel diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index bc55fda784c..3cacb773acc 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -246,6 +246,7 @@ public struct ChatListNodeState: Equatable { public var pendingRemovalItemIds: Set public var pendingClearHistoryPeerIds: Set public var hiddenItemShouldBeTemporaryRevealed: Bool + public var archiveParams: ArchiveAnimationParams public var selectedAdditionalCategoryIds: Set public var hiddenPsaPeerId: EnginePeer.Id? public var foundPeers: [(EnginePeer, EnginePeer?)] @@ -265,6 +266,7 @@ public struct ChatListNodeState: Equatable { pendingRemovalItemIds: Set, pendingClearHistoryPeerIds: Set, hiddenItemShouldBeTemporaryRevealed: Bool, + archiveParams: ArchiveAnimationParams, hiddenPsaPeerId: EnginePeer.Id?, selectedThreadIds: Set, archiveStoryState: StoryState? @@ -280,6 +282,7 @@ public struct ChatListNodeState: Equatable { self.pendingRemovalItemIds = pendingRemovalItemIds self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed + self.archiveParams = archiveParams self.hiddenPsaPeerId = hiddenPsaPeerId self.selectedThreadIds = selectedThreadIds self.archiveStoryState = archiveStoryState @@ -319,6 +322,9 @@ public struct ChatListNodeState: Equatable { if lhs.hiddenItemShouldBeTemporaryRevealed != rhs.hiddenItemShouldBeTemporaryRevealed { return false } + if lhs.archiveParams != rhs.archiveParams { + return false + } if lhs.hiddenPsaPeerId != rhs.hiddenPsaPeerId { return false } @@ -419,6 +425,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL header: nil, enableContextActions: true, hiddenOffset: threadInfo?.isHidden == true && !revealed, + params: .emptyVisibleParams, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): @@ -624,6 +631,11 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case let .HoleEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(groupReferenceEntry): +// if groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed { +// return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveTransitionItem(theme: groupReferenceEntry.presentationData.theme), +// directionHint: entry.directionHint) +// } else { + print("insert group entry which hiddenByDefault: \(groupReferenceEntry.hiddenByDefault) revealed: \(groupReferenceEntry.revealed) params: \(groupReferenceEntry.archiveParams)") return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: groupReferenceEntry.presentationData, context: context, @@ -649,8 +661,11 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL header: nil, enableContextActions: true, hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, + params: groupReferenceEntry.archiveParams, interaction: nodeInteraction ), directionHint: entry.directionHint) + +// } case let .ContactEntry(contactEntry): let header: ChatListSearchItemHeader? = nil @@ -776,6 +791,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL header: nil, enableContextActions: true, hiddenOffset: threadInfo?.isHidden == true && !revealed, + params: .emptyVisibleParams, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): @@ -935,6 +951,12 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL case let .HoleEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(groupReferenceEntry): +// if groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed { +// return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveTransitionItem(theme: groupReferenceEntry.presentationData.theme), +// directionHint: entry.directionHint) +// } else { + print("update group entry which hiddenByDefault: \(groupReferenceEntry.hiddenByDefault) revealed: \(groupReferenceEntry.revealed) top offset: \(groupReferenceEntry.archiveParams)") + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: groupReferenceEntry.presentationData, context: context, @@ -960,8 +982,10 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL header: nil, enableContextActions: true, hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, + params: groupReferenceEntry.archiveParams, interaction: nodeInteraction ), directionHint: entry.directionHint) +// } case let .ContactEntry(contactEntry): let header: ChatListSearchItemHeader? = nil @@ -1253,7 +1277,7 @@ public final class ChatListNode: ListView { isSelecting = true } - self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), archiveStoryState: nil) + self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, archiveParams: .emptyVisibleParams, hiddenPsaPeerId: nil, selectedThreadIds: Set(), archiveStoryState: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme @@ -1633,6 +1657,18 @@ public final class ChatListNode: ListView { return settings.isHiddenByDefault } |> distinctUntilChanged + |> afterNext { [weak self] value in + Queue.mainQueue().async { + self?.updateState { state in + var state = state + var updatedParams = state.archiveParams + updatedParams.updateVisibility(isHiddenByDefault: value) + state.archiveParams = updatedParams + return state + } + } + } + let displayArchiveIntro: Signal if case .chatList(.archive) = location { @@ -2351,6 +2387,9 @@ public final class ChatListNode: ListView { if didIncludeNotice != doesIncludeNotice { disableAnimations = false } + if previousState.archiveParams != state.archiveParams { + disableAnimations = false + } } if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault { @@ -2435,9 +2474,15 @@ public final class ChatListNode: ListView { } } if !isHiddenItemVisible && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { + strongSelf.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false + state.archiveParams = .init(scrollOffset: .zero, + storiesFraction: .zero, + expandedHeight: .zero, + finalizeAnimation: false, + isHiddenByDefault: state.archiveParams.isHiddenByDefault) return state } } @@ -2913,7 +2958,36 @@ public final class ChatListNode: ListView { return isHiddenItemVisible } - func revealScrollHiddenItem() { +// func revealScrollHiddenItem() { +// var isHiddenItemVisible = false +// self.forEachItemNode({ itemNode in +// if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { +// if case let .peer(peerData) = item.content, let threadInfo = peerData.threadInfo { +// if threadInfo.isHidden { +// isHiddenItemVisible = true +// } +// } +// if case let .groupReference(groupReference) = item.content { +// if groupReference.hiddenByDefault { +// isHiddenItemVisible = true +// } +// } +// } +// }) +// if isHiddenItemVisible && !self.currentState.hiddenItemShouldBeTemporaryRevealed { +// if self.hapticFeedback == nil { +// self.hapticFeedback = HapticFeedback() +// } +// self.hapticFeedback?.impact(.medium) +// self.updateState { state in +// var state = state +// state.hiddenItemShouldBeTemporaryRevealed = true +// return state +// } +// } +// } + + func updateArchiveParams(params: ArchiveAnimationParams) { var isHiddenItemVisible = false self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { @@ -2922,23 +2996,35 @@ public final class ChatListNode: ListView { isHiddenItemVisible = true } } + if case let .groupReference(groupReference) = item.content { if groupReference.hiddenByDefault { isHiddenItemVisible = true } + + if groupReference.groupId == .archive && !item.params.isArchiveGroupVisible { + isHiddenItemVisible = true + } } } }) - if isHiddenItemVisible && !self.currentState.hiddenItemShouldBeTemporaryRevealed { - if self.hapticFeedback == nil { - self.hapticFeedback = HapticFeedback() - } - self.hapticFeedback?.impact(.medium) - self.updateState { state in - var state = state - state.hiddenItemShouldBeTemporaryRevealed = true - return state - } + guard isHiddenItemVisible else { + print("isHiddenItemVisible: \(isHiddenItemVisible)") + return + } + + let hiddenItemShouldBeTemporaryRevealed = params.finalizeAnimation ? params.isArchiveGroupVisible : true + if hiddenItemShouldBeTemporaryRevealed != self.currentState.hiddenItemShouldBeTemporaryRevealed { + print("hiddenItemShouldBeTemporaryRevealed: \(hiddenItemShouldBeTemporaryRevealed)") + } + + self.updateState { state in + var state = state + var params = params + params.updateVisibility(isRevealed: hiddenItemShouldBeTemporaryRevealed) + state.archiveParams = params + state.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed + return state } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index bf851892c7d..18143bc90aa 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -312,6 +312,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { var unreadCount: Int var revealed: Bool var hiddenByDefault: Bool + var archiveParams: ArchiveAnimationParams var storyState: ChatListNodeState.StoryState? init( @@ -324,6 +325,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { unreadCount: Int, revealed: Bool, hiddenByDefault: Bool, + archiveParams: ArchiveAnimationParams, storyState: ChatListNodeState.StoryState? ) { self.index = index @@ -335,6 +337,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { self.unreadCount = unreadCount self.revealed = revealed self.hiddenByDefault = hiddenByDefault + self.archiveParams = archiveParams self.storyState = storyState } @@ -370,6 +373,10 @@ enum ChatListNodeEntry: Comparable, Identifiable { return false } + if lhs.archiveParams != rhs.archiveParams { + return false + } + return true } } @@ -847,6 +854,11 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, if let archiveStoryState = state.archiveStoryState { mappedStoryState = archiveStoryState } + var params = state.archiveParams + if params.isHiddenByDefault != hideArchivedFolderByDefault { + params.updateVisibility(isRevealed: state.hiddenItemShouldBeTemporaryRevealed, isHiddenByDefault: hideArchivedFolderByDefault) + } + result.append(.GroupReferenceEntry(ChatListNodeEntry.GroupReferenceEntryData( index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: messageIndex)), presentationData: state.presentationData, @@ -857,6 +869,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, unreadCount: groupReference.unreadCount, revealed: state.hiddenItemShouldBeTemporaryRevealed, hiddenByDefault: hideArchivedFolderByDefault, + archiveParams: params, storyState: mappedStoryState ))) if pinningIndex != 0 { diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 35d1cee10a4..eda95c8571f 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -389,7 +389,7 @@ public func generateGradientImage(size: CGSize, scale: CGFloat = 0.0, colors: [U context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } - let image = UIGraphicsGetImageFromCurrentImageContext()! + let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index d2ffb161b31..d108e15a091 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -298,6 +298,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView header: nil, enableContextActions: false, hiddenOffset: false, + params: .emptyVisibleParams, interaction: interaction ) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 6b4d9af2bb3..ddf7f00a5a7 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -931,6 +931,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate header: nil, enableContextActions: false, hiddenOffset: false, + params: .emptyVisibleParams, interaction: interaction ) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index e48ad2ef67b..90ad3276ee9 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -444,6 +444,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { header: nil, enableContextActions: false, hiddenOffset: false, + params: .emptyVisibleParams, interaction: interaction ) } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index c9a2e45a2b6..403e6ff77f6 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -509,8 +509,8 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati verifiedIconFillColor: UIColor(rgb: 0xffffff), verifiedIconForegroundColor: UIColor(rgb: 0x000000), secretIconColor: UIColor(rgb: 0x00b12c), - pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), - unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x666666), bottomColor: UIColor(rgb: 0x666666)), foregroundColor: UIColor(rgb: 0x000000)), + pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x60b7fc), bottomColor: UIColor(rgb: 0x027df0)), foregroundColor: UIColor(rgb: 0xffffff)), + unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0xb2b3b7), bottomColor: UIColor(rgb: 0x8c8e92)), foregroundColor: UIColor(rgb: 0x000000)), onlineDotColor: UIColor(rgb: 0x4cc91f), storyUnseenColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x34C76F), bottomColor: UIColor(rgb: 0x3DA1FD)), storyUnseenPrivateColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x7CD636), bottomColor: UIColor(rgb: 0x26B470)), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index b5fcd56959f..7a52fbcab22 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -567,8 +567,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio verifiedIconFillColor: defaultDayAccentColor, verifiedIconForegroundColor: UIColor(rgb: 0xffffff), secretIconColor: UIColor(rgb: 0x00b12c), - pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), - unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0xdedee5), bottomColor: UIColor(rgb: 0xc5c6cc)), foregroundColor: UIColor(rgb: 0xffffff)), + pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x60b7fc), bottomColor: UIColor(rgb: 0x027df0)), foregroundColor: UIColor(rgb: 0xffffff)), + unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0xd9d9de), bottomColor: UIColor(rgb: 0xb0b6be)), foregroundColor: UIColor(rgb: 0xffffff)), onlineDotColor: UIColor(rgb: 0x4cc91f), storyUnseenColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x34C76F), bottomColor: UIColor(rgb: 0x3DA1FD)), storyUnseenPrivateColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x7CD636), bottomColor: UIColor(rgb: 0x26B470)), diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 9d009cdc176..41bf6b8e961 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -99,6 +99,24 @@ public struct PresentationResourcesItemList { }) } + public static func archiveTransitionArrowIcon(_ theme: PresentationTheme, backgroundColor: UIColor) -> UIImage? { + guard + let icon = generateTintedImage( + image: UIImage(bundleImageName: "Chat List/Archive/IconArrow"), + color: theme.chatList.pinnedArchiveAvatarColor.foregroundColor + ) + else { return nil } + + return generateImage(icon.size, contextGenerator: { size, context in + if let iconCgImage = icon.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 2, y: 2), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) + context.draw(iconCgImage, in: CGRect(origin: CGPoint(), size: size)) + } + }) + } + public static func itemListDeleteIndicatorIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListDeleteIndicatorIcon.rawValue, { theme in guard let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/RemoveItemIcon"), color: theme.list.itemDestructiveColor) else { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 348ed018b68..ad6573edc9e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -349,12 +349,13 @@ public final class StoryPeerListComponent: Component { private var animationState: AnimationState? private var animator: ConstantDisplayLinkAnimator? - private var currentFraction: CGFloat = 0.0 + public private(set) var currentFraction: CGFloat = 0.0 private var currentTitleWidth: CGFloat = 0.0 private var currentActivityFraction: CGFloat = 0.0 public private(set) var overscrollSelectedId: EnginePeer.Id? public private(set) var overscrollHiddenChatItemsAllowed: Bool = false + public private(set) var overscrollFraction: CGFloat = 0.0 private var anchorForTooltipRect: CGRect? @@ -832,6 +833,8 @@ public final class StoryPeerListComponent: Component { } } +// print("overscrollStage1: \(overscrollStage1) overscrollStage2: \(overscrollStage2) realTimeOverscrollFraction: \(realTimeOverscrollFraction)") + self.overscrollFraction = overscrollStage1 if overscrollStage1 >= 0.5 { self.overscrollHiddenChatItemsAllowed = true } else { diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/Contents.json index adc5e58e9c9..f629023a91d 100644 --- a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/Contents.json @@ -1,22 +1,15 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "archiveavatar@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "archiveavatar@3x.png", - "scale" : "3x" + "filename" : "archiveavatar.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar.pdf new file mode 100644 index 00000000000..d0dacb92c92 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@2x.png b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@2x.png deleted file mode 100644 index ceff2065d3b..00000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@3x.png b/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@3x.png deleted file mode 100644 index 5bdb155bf18..00000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Avatar/ArchiveAvatarIcon.imageset/archiveavatar@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/Contents.json new file mode 100644 index 00000000000..63fa90a793d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/ic_arrow.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/ic_arrow.pdf new file mode 100644 index 00000000000..663eb37f931 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Archive/IconArrow.imageset/ic_arrow.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_arrow_to_archive.json b/submodules/TelegramUI/Resources/Animations/anim_arrow_to_archive.json new file mode 100644 index 00000000000..a897adb1c42 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_arrow_to_archive.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":60,"ip":0,"op":60,"w":180,"h":180,"nm":"archiveicon 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Arrow 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[90]},{"t":45,"s":[90]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[95.09]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[81.698]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[84.496]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[83.879]},{"t":45,"s":[84.017]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4.5,-2.25],[0,2.25],[-4.5,-2.25]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,0],[0,0],[-4,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Arrow 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Arrow 1","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-8.25,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,4],[0,-4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Arrow 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[15]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":10,"s":[15]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":60,"st":-60,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Cap","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[90,101.445,0],"to":[0,-7.737,0],"ti":[0,6.121,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[90,55.02,0],"to":[0,-6.121,0],"ti":[0,-1.26,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[90,64.72,0],"to":[0,1.26,0],"ti":[0,0.277,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[90,62.579,0],"to":[0,-0.277,0],"ti":[0,-0.08,0]},{"t":45,"s":[90,63.059,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":2,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[20.083,19.766]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[25.802,4.562]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":25,"s":[26.014,3.96]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":35,"s":[25.995,4.014]},{"t":45,"s":[26,3.999]}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,0.021],"to":[0,0.155],"ti":[0,-0.164]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[0,0.95],"to":[0,0.164],"ti":[0,-0.008]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[0,1.004],"to":[0,0.008],"ti":[0,0.001]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0,0.999],"to":[0,-0.001],"ti":[0,0]},{"t":45,"s":[0,1]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[9.876]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[1.298]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0.979]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[1.007]},{"t":45,"s":[1]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"cap2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[20.083,19.807]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[25.802,6.463]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":25,"s":[26.014,5.967]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":35,"s":[25.995,6.011]},{"t":45,"s":[26,5.999]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[9.903]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[3.231]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[2.983]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[3.006]},{"t":45,"s":[3]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"cap1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Box","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[90,102,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[20.057,19.972]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[24.819,17.591]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":25,"s":[23.824,18.088]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":35,"s":[24.043,17.978]},{"t":45,"s":[23.994,18.003]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[9.903]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[3.231]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[2.983]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[3.006]},{"t":45,"s":[3]}],"ix":4},"nm":"Box 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[20.057,19.858]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[24.819,7.954]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":25,"s":[23.824,10.441]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":35,"s":[24.043,9.892]},{"t":45,"s":[23.994,10.015]}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,-0.057],"to":[0,-0.794],"ti":[0,0.628]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[0,-4.819],"to":[0,-0.628],"ti":[0,-0.129]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[0,-3.824],"to":[0,0.129],"ti":[0,0.028]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0,-4.043],"to":[0,-0.028],"ti":[0,-0.008]},{"t":45,"s":[0,-3.994]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[9.876]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[1.298]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0.979]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[1.007]},{"t":45,"s":[1]}],"ix":4},"nm":"Box 2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9ec50462755..d236f0d5980 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -11271,6 +11271,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } + if let arguments = self.presentationArguments as? ChatListPreviewPresentationData, + let (sourceNode, sourceRect) = arguments.sourceNodeAndRect() + { + let containerNode = ASDisplayNode() + containerNode.backgroundColor = .white.withAlphaComponent(0.5) + + sourceNode.backgroundColor = .red + self.chatDisplayNode.bounds = sourceRect + self.chatDisplayNode.position = sourceRect.center + } + if !self.didSetup3dTouch { self.didSetup3dTouch = true if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 9659de86ed6..09fd25d2279 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -111,6 +111,7 @@ private enum ChatListSearchEntry: Comparable, Identifiable { header: nil, enableContextActions: false, hiddenOffset: false, + params: .emptyVisibleParams, interaction: interaction ) }