diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index b378d6b817..be1d3e8a6e 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -598,21 +598,21 @@ public extension ContainedViewLayoutTransition { } } - func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { + func animatePositionAdditive(layer: CALayer, offset: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { switch self { case .immediate: completion(true) case let .animated(duration, curve): - layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } } - func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { + func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { switch self { case .immediate: completion?() case let .animated(duration, curve): - node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in + node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in completion?() }) } @@ -948,7 +948,7 @@ public extension ContainedViewLayoutTransition { } } - func animateTransformScale(node: ASDisplayNode, from fromScale: CGPoint, completion: ((Bool) -> Void)? = nil) { + func animateTransformScale(node: ASDisplayNode, from fromScale: CGPoint, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { @@ -961,12 +961,12 @@ public extension ContainedViewLayoutTransition { calculatedFrom = fromScale calculatedTo = CGPoint(x: 1.0, y: 1.0) - node.layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + node.layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) - node.layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + node.layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) } } diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 1cc4014f82..3f43211ee1 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -947,6 +947,7 @@ public final class MessageHistoryView { public let topTaggedMessages: [Message] public let additionalData: [AdditionalMessageHistoryViewDataEntry] public let isLoading: Bool + public let isLoadingEarlier: Bool public let isAddedToChatList: Bool public init(tagMask: MessageTags?, namespaces: MessageIdNamespaces, entries: [MessageHistoryEntry], holeEarlier: Bool, holeLater: Bool, isLoading: Bool) { @@ -964,6 +965,7 @@ public final class MessageHistoryView { self.topTaggedMessages = [] self.additionalData = [] self.isLoading = isLoading + self.isLoadingEarlier = true self.isAddedToChatList = false } @@ -981,8 +983,10 @@ public final class MessageHistoryView { self.holeLater = true self.earlierId = nil self.laterId = nil + self.isLoadingEarlier = true case let .loaded(state): var isLoading = false + var isLoadingEarlier = false switch state.anchor { case .lowerBound: self.anchorIndex = .lowerBound @@ -996,6 +1000,10 @@ public final class MessageHistoryView { if state.entries.isEmpty && state.hole != nil { isLoading = true } + if state.entries.count <= 1 && state.hole != nil { + isLoadingEarlier = true + } + self.isLoadingEarlier = isLoadingEarlier entries = [] if let transientReadStates = mutableView.transientReadStates, case let .peer(states) = transientReadStates { for entry in state.entries { @@ -1199,6 +1207,7 @@ public final class MessageHistoryView { self.topTaggedMessages = base.topTaggedMessages self.additionalData = base.additionalData self.isLoading = base.isLoading + self.isLoadingEarlier = base.isLoadingEarlier self.isAddedToChatList = base.isAddedToChatList if let combinedReadStates = combinedReadStates { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9769d50761..cdbfb325d6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5866,6 +5866,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + strongSelf.chatDisplayNode.loadingPlaceholderNode?.addContentOffset(offset: offset, transition: transition) strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 547e196ec5..4aab5705e0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -92,7 +92,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var blurredHistoryNode: ASImageNode? let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode - private var loadingPlaceholderNode: ChatLoadingPlaceholderNode + private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? private var emptyNode: ChatEmptyNode? private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType? @@ -222,49 +222,66 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var isLoadingValue: Bool = false private func updateIsLoading(isLoading: Bool, animated: Bool) { - loadingPlaceholderNode.isHidden = true + let useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser + if isLoading != self.isLoadingValue { self.isLoadingValue = isLoading if isLoading { -// self.historyNodeContainer.supernode?.insertSubnode(self.loadingNode, belowSubnode: self.historyNodeContainer) - self.loadingNode.isHidden = false - self.loadingNode.layer.removeAllAnimations() - self.loadingNode.alpha = 1.0 - if animated { - self.loadingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - - self.loadingPlaceholderNode.alpha = 1.0 - self.loadingPlaceholderNode.isHidden = false - self.historyNode.alpha = 0.0 - } else { - self.loadingPlaceholderNode.alpha = 0.0 - self.loadingPlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in - if let strongSelf = self { - strongSelf.loadingPlaceholderNode.layer.removeAllAnimations() - if completed { - strongSelf.loadingPlaceholderNode.isHidden = true + if useLoadingPlaceholder { + let loadingPlaceholderNode: ChatLoadingPlaceholderNode + if let current = self.loadingPlaceholderNode { + loadingPlaceholderNode = current + } else { + loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode) + loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState) + self.contentContainerNode.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode) + + self.loadingPlaceholderNode = loadingPlaceholderNode + + loadingPlaceholderNode.setup(self.historyNode) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate, listViewTransaction: { _, _, _, _ in + }, updateExtraNavigationBarBackgroundHeight: { _, _ in + }) } } - }) - self.loadingPlaceholderNode.animateOut(self.historyNode) - - self.historyNode.alpha = 1.0 - self.historyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - self.loadingNode.alpha = 0.0 - if animated { - self.loadingNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) - self.loadingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in - if let strongSelf = self { - strongSelf.loadingNode.layer.removeAllAnimations() - if completed { - strongSelf.loadingNode.isHidden = true - } - } - }) + loadingPlaceholderNode.alpha = 1.0 + loadingPlaceholderNode.isHidden = false } else { - self.loadingNode.isHidden = true + self.historyNodeContainer.supernode?.insertSubnode(self.loadingNode, belowSubnode: self.historyNodeContainer) + self.loadingNode.isHidden = false + self.loadingNode.layer.removeAllAnimations() + self.loadingNode.alpha = 1.0 + if animated { + self.loadingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + } else { + if useLoadingPlaceholder { + if let loadingPlaceholderNode = self.loadingPlaceholderNode { + loadingPlaceholderNode.animateOut(self.historyNode, completion: { [weak self] in + if let strongSelf = self { + strongSelf.loadingPlaceholderNode?.removeFromSupernode() + strongSelf.loadingPlaceholderNode = nil + } + }) + } + } else { + self.loadingNode.alpha = 0.0 + if animated { + self.loadingNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) + self.loadingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in + if let strongSelf = self { + strongSelf.loadingNode.layer.removeAllAnimations() + if completed { + strongSelf.loadingNode.isHidden = true + } + } + }) + } else { + self.loadingNode.isHidden = true + } } } } @@ -414,9 +431,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners) - - self.loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode, hasAvatar: chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser) - + self.inputPanelContainerNode = ChatInputPanelContainer() self.inputPanelOverlayNode = SparseNode() self.inputPanelClippingNode = SparseNode() @@ -490,13 +505,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } assert(Queue.mainQueue().isCurrent()) - -// self.updateIsLoading(isLoading: true, animated: false) -// -// Queue.mainQueue().after(1.0) { -// self.updateIsLoading(isLoading: false, animated: true) -// } - + self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in if let strongSelf = self { let wasLoading = strongSelf.isLoadingValue @@ -561,12 +570,19 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.contentContainerNode) self.contentContainerNode.addSubnode(self.backgroundNode) - self.contentContainerNode.addSubnode(self.loadingPlaceholderNode) self.contentContainerNode.addSubnode(self.historyNodeContainer) if let navigationBar = self.navigationBar { self.contentContainerNode.addSubnode(navigationBar) } + +// Queue.mainQueue().after(0.2) { +// self.updateIsLoading(isLoading: true, animated: false) +// } +// +// Queue.mainQueue().after(3.0) { +// self.updateIsLoading(isLoading: false, animated: true) +// } self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in guard let strongSelf = self else { @@ -1417,7 +1433,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { //transition.updateFrame(node: self.historyScrollingArea, frame: contentBounds) transition.updateFrame(node: self.loadingNode, frame: contentBounds) - transition.updateFrame(node: self.loadingPlaceholderNode, frame: contentBounds) + if let loadingPlaceholderNode = self.loadingPlaceholderNode { + transition.updateFrame(node: loadingPlaceholderNode, frame: contentBounds) + } if let restrictedNode = self.restrictedNode { transition.updateFrame(node: restrictedNode, frame: contentBounds) @@ -1575,8 +1593,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.visibleAreaInset = visibleAreaInset self.loadingNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, transition: transition) - self.loadingPlaceholderNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, transition: transition) - self.loadingPlaceholderNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) + if let loadingPlaceholderNode = self.loadingPlaceholderNode { + loadingPlaceholderNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, transition: transition) + loadingPlaceholderNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) + } if let containerNode = self.containerNode { contentBottomInset += 8.0 @@ -2210,6 +2230,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper) self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) + self.loadingPlaceholderNode?.updatePresentationInterfaceState(chatPresentationInterfaceState) var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) if self.chatPresentationInterfaceStateInputView(self.chatPresentationInterfaceState) !== self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index c6eea5ff4e..004216cc5c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2559,7 +2559,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { loadState = .empty(emptyType) } } else { - loadState = .messages + if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser { + loadState = .loading + } else { + loadState = .messages + } } } else { loadState = .loading @@ -2639,12 +2643,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf._buttonKeyboardMessage.set(.single(transition.keyboardButtonsMessage)) } - if transition.animateIn || animateIn { + if (transition.animateIn || animateIn) && !"".isEmpty { let heightNorm = strongSelf.bounds.height - strongSelf.insets.top strongSelf.forEachVisibleItemNode { itemNode in let delayFactor = itemNode.frame.minY / heightNorm let delay = Double(delayFactor * 0.1) - + if let itemNode = itemNode as? ChatMessageItemView { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) @@ -2657,7 +2661,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.forEachItemHeaderNode { itemNode in let delayFactor = itemNode.frame.minY / heightNorm let delay = Double(delayFactor * 0.2) - + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) } diff --git a/submodules/TelegramUI/Sources/ChatLoadingNode.swift b/submodules/TelegramUI/Sources/ChatLoadingNode.swift index 2059a09b9c..64d85470e9 100644 --- a/submodules/TelegramUI/Sources/ChatLoadingNode.swift +++ b/submodules/TelegramUI/Sources/ChatLoadingNode.swift @@ -7,6 +7,7 @@ import TelegramPresentationData import ActivityIndicator import WallpaperBackgroundNode import ShimmerEffect +import ChatPresentationInterfaceState final class ChatLoadingNode: ASDisplayNode { private let backgroundNode: NavigationBackgroundNode @@ -47,10 +48,12 @@ final class ChatLoadingNode: ASDisplayNode { } private let avatarSize = CGSize(width: 38.0, height: 38.0) +private let avatarImage = generateFilledCircleImage(diameter: avatarSize.width, color: .white) +private let avatarBorderImage = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .white) final class ChatLoadingPlaceholderMessageContainer { - let avatarNode: ASImageNode? - let avatarBorderNode: ASImageNode? + var avatarNode: ASImageNode? + var avatarBorderNode: ASImageNode? let bubbleNode: ASImageNode let bubbleBorderNode: ASImageNode @@ -63,20 +66,7 @@ final class ChatLoadingPlaceholderMessageContainer { return self.bubbleNode.frame } - init(hasAvatar: Bool, bubbleImage: UIImage?, bubbleBorderImage: UIImage?) { - if hasAvatar { - self.avatarNode = ASImageNode() - self.avatarNode?.displaysAsynchronously = false - self.avatarNode?.image = generateFilledCircleImage(diameter: avatarSize.width, color: .white) - - self.avatarBorderNode = ASImageNode() - self.avatarBorderNode?.displaysAsynchronously = false - self.avatarBorderNode?.image = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .red) - } else { - self.avatarNode = nil - self.avatarBorderNode = nil - } - + init(bubbleImage: UIImage?, bubbleBorderImage: UIImage?) { self.bubbleNode = ASImageNode() self.bubbleNode.displaysAsynchronously = false self.bubbleNode.image = bubbleImage @@ -87,38 +77,59 @@ final class ChatLoadingPlaceholderMessageContainer { } func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) { - if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode { - maskNode.addSubnode(avatarNode) - borderMaskNode.addSubnode(avatarBorderNode) - } maskNode.addSubnode(self.bubbleNode) borderMaskNode.addSubnode(self.bubbleBorderNode) } - func animateWith(_ listItemNode: ListViewItemNode, transition: ContainedViewLayoutTransition) { + func animateWith(_ listItemNode: ListViewItemNode, delay: Double, transition: ContainedViewLayoutTransition) { + listItemNode.allowsGroupOpacity = true + listItemNode.alpha = 1.0 + listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay, completion: { _ in + listItemNode.allowsGroupOpacity = false + }) + if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode { - bubbleItemNode.animateFromLoadingPlaceholder(messageContainer: self, transition: transition) + bubbleItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + } else if let stickerItemNode = listItemNode as? ChatMessageStickerItemNode { + stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) } else if let stickerItemNode = listItemNode as? ChatMessageAnimatedStickerItemNode { - stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, transition: transition) + stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + } else if let videoItemNode = listItemNode as? ChatMessageInstantVideoItemNode { + videoItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) } } func animateBackgroundFrame(to frame: CGRect, transition: ContainedViewLayoutTransition) { - let targetFrame = CGRect(origin: CGPoint(x: self.bubbleNode.frame.minX, y: frame.minY), size: frame.size) - - transition.updateFrame(node: self.bubbleNode, frame: targetFrame) - transition.updateFrame(node: self.bubbleBorderNode, frame: targetFrame) - - if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode { - let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: frame.maxY + 1.0 - avatarSize.height), size: avatarSize) - - transition.updateFrame(node: avatarNode, frame: avatarFrame) - transition.updateFrame(node: avatarBorderNode, frame: avatarFrame) - } +// let targetFrame = CGRect(origin: CGPoint(x: self.bubbleNode.frame.minX, y: frame.minY), size: frame.size) +// +// transition.updateFrame(node: self.bubbleNode, frame: targetFrame) +// transition.updateFrame(node: self.bubbleBorderNode, frame: targetFrame) +// +// if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode { +// let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: frame.maxY + 1.0 - avatarSize.height), size: avatarSize) +// +// transition.updateFrame(node: avatarNode, frame: avatarFrame) +// transition.updateFrame(node: avatarBorderNode, frame: avatarFrame) +// } } - func update(size: CGSize, rect: CGRect, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) { var avatarOffset: CGFloat = 0.0 + + if hasAvatar && self.avatarNode == nil { + let avatarNode = ASImageNode() + avatarNode.displaysAsynchronously = false + avatarNode.image = avatarImage + self.bubbleNode.supernode?.addSubnode(avatarNode) + self.avatarNode = avatarNode + + let avatarBorderNode = ASImageNode() + avatarBorderNode.displaysAsynchronously = false + avatarBorderNode.image = avatarBorderImage + self.bubbleBorderNode.supernode?.addSubnode(avatarBorderNode) + self.avatarBorderNode = avatarBorderNode + } + if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode { let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: rect.maxY + 1.0 - avatarSize.height), size: avatarSize) @@ -140,6 +151,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { private let maskNode: ASDisplayNode private let borderMaskNode: ASDisplayNode + private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private var backgroundContent: WallpaperBubbleBackgroundNode? private let backgroundColorNode: ASDisplayNode @@ -154,8 +166,10 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { private var validLayout: (CGSize, UIEdgeInsets)? - init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode, hasAvatar: Bool) { + init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) { self.backgroundNode = backgroundNode + + self.scrollingContainer = ASDisplayNode() self.maskNode = ASDisplayNode() self.borderMaskNode = ASDisplayNode() @@ -164,7 +178,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { var messageContainers: [ChatLoadingPlaceholderMessageContainer] = [] for _ in 0 ..< 8 { - let container = ChatLoadingPlaceholderMessageContainer(hasAvatar: hasAvatar, bubbleImage: bubbleImage, bubbleBorderImage: bubbleBorderImage) + let container = ChatLoadingPlaceholderMessageContainer(bubbleImage: bubbleImage, bubbleBorderImage: bubbleBorderImage) container.setup(maskNode: self.maskNode, borderMaskNode: self.borderMaskNode) messageContainers.append(container) } @@ -184,14 +198,14 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { super.init() - self.addSubnode(self.containerNode) + self.addSubnode(self.scrollingContainer) + + self.scrollingContainer.addSubnode(self.containerNode) self.containerNode.addSubnode(self.backgroundColorNode) self.containerNode.addSubnode(self.effectNode) - self.addSubnode(self.borderNode) + self.scrollingContainer.addSubnode(self.borderNode) self.borderNode.addSubnode(self.borderEffectNode) -// self.addSubnode(self.maskNode) -// self.addSubnode(self.borderMaskNode) } override func didLoad() { @@ -200,73 +214,174 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { self.containerNode.view.mask = self.maskNode.view self.borderNode.view.mask = self.borderMaskNode.view -// self.backgroundNode?.updateIsLooping(true) + self.backgroundNode?.updateIsLooping(true) } - func animateOut(_ historyNode: ChatHistoryNode) { + private var bottomInset: (Int, CGFloat)? + func setup(_ historyNode: ChatHistoryNode) { + guard let listNode = historyNode as? ListView else { + return + } + + var count = 0 + var inset: CGFloat = 0.0 + listNode.forEachVisibleItemNode { itemNode in + inset += itemNode.frame.height + count += 1 + } + + if count > 0 { + self.bottomInset = (count, inset) + } + + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: inset) + } + + func animateOut(_ historyNode: ChatHistoryNode, completion: @escaping () -> Void = {}) { guard let listNode = historyNode as? ListView, let (size, _) = self.validLayout else { return } self.backgroundNode?.updateIsLooping(false) - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring) var lastFrame: CGRect? - var i = 0 + let heightNorm = listNode.bounds.height - listNode.insets.top + + var index = 0 + var skipCount = self.bottomInset?.0 ?? 0 listNode.forEachVisibleItemNode { itemNode in - guard i < self.messageContainers.count, let listItemNode = itemNode as? ListViewItemNode else { + guard index < self.messageContainers.count, let listItemNode = itemNode as? ListViewItemNode else { + return + } + + let delayFactor = listItemNode.frame.minY / heightNorm + let delay = Double(delayFactor * 0.1) + + if skipCount > 0 { + skipCount -= 1 return } - if let bubbleItemNode = itemNode as? ChatMessageBubbleItemNode, - bubbleItemNode.contentNodes.contains(where: { $0 is ChatMessageActionBubbleContentNode }) { + if let itemNode = itemNode as? ChatUnreadItemNode { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0) + return + } + if let itemNode = itemNode as? ChatReplyCountItemNode { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0) return } - let messageContainer = self.messageContainers[i] - messageContainer.animateWith(listItemNode, transition: transition) + if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode, bubbleItemNode.contentNodes.contains(where: { $0 is ChatMessageActionBubbleContentNode }) { + return + } + + let messageContainer = self.messageContainers[index] + messageContainer.animateWith(listItemNode, delay: delay, transition: transition) lastFrame = messageContainer.frame - i += 1 + index += 1 } - if let lastFrame = lastFrame, i < self.messageContainers.count { + skipCount = self.bottomInset?.0 ?? 0 + listNode.forEachItemHeaderNode { itemNode in + var animateScale = true + if itemNode is ChatMessageAvatarHeaderNode { + animateScale = false + if skipCount > 0 { + return + } + } + if itemNode is ChatMessageDateHeaderNode { + if skipCount > 0 { + skipCount -= 1 + return + } + } + + let delayFactor = itemNode.frame.minY / heightNorm + let delay = Double(delayFactor * 0.2) + + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + if animateScale { + itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + + if let lastFrame = lastFrame, index < self.messageContainers.count { var offset = lastFrame.minY - for k in i ..< self.messageContainers.count { + for k in index ..< self.messageContainers.count { let messageContainer = self.messageContainers[k] let messageSize = messageContainer.frame.size - messageContainer.update(size: size, rect: CGRect(origin: CGPoint(x: 0.0, y: offset - messageSize.height), size: messageSize), transition: transition) + messageContainer.update(size: size, hasAvatar: self.isGroup, rect: CGRect(origin: CGPoint(x: 0.0, y: offset - messageSize.height), size: messageSize), transition: transition) offset -= messageSize.height } } } - public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + private var ignoreFirstContentOffset = true + func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + guard !self.ignoreFirstContentOffset else { + self.ignoreFirstContentOffset = false + return + } + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) + if let (rect, containerSize) = self.absolutePosition { + self.update(rect: rect, within: containerSize) + } + } + + func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY + backgroundFrame.origin.y += rect.minY - self.scrollingContainer.bounds.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) } } + private var isGroup = false + func updatePresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) { + var isGroup = false + if let peer = chatPresentationInterfaceState.renderedPeer?.peer { + if peer is TelegramGroup { + isGroup = true + } else if let channel = peer as? TelegramChannel, case .group = channel.info { + isGroup = true + } + } + + if self.isGroup != isGroup { + self.isGroup = isGroup + if let (size, insets) = self.validLayout { + self.updateLayout(size: size, insets: insets, transition: .immediate) + } + } + } + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets) let bounds = CGRect(origin: .zero, size: size) + transition.updateFrame(node: self.scrollingContainer, frame: bounds) + transition.updateFrame(node: self.maskNode, frame: bounds) transition.updateFrame(node: self.borderMaskNode, frame: bounds) transition.updateFrame(node: self.containerNode, frame: bounds) transition.updateFrame(node: self.borderNode, frame: bounds) - transition.updateFrame(node: self.backgroundColorNode, frame: bounds) - transition.updateFrame(node: self.effectNode, frame: bounds) transition.updateFrame(node: self.borderEffectNode, frame: bounds) @@ -291,15 +406,19 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { ] var offset: CGFloat = 5.0 - var i = 0 - for messageContainer in self.messageContainers { - let messageSize = dimensions[i % 12] - messageContainer.update(size: size, rect: CGRect(origin: CGPoint(x: 0.0, y: size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition) - offset += messageSize.height - i += 1 + var index = 0 + if let (insetCount, _) = self.bottomInset { + index += insetCount } - if backgroundNode?.hasExtraBubbleBackground() == true { + for messageContainer in self.messageContainers { + let messageSize = dimensions[index % 8] + messageContainer.update(size: size, hasAvatar: self.isGroup, rect: CGRect(origin: CGPoint(x: 0.0, y: size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition) + offset += messageSize.height + index += 1 + } + + if self.backgroundNode?.hasExtraBubbleBackground() == true { self.backgroundColorNode.isHidden = true } else { self.backgroundColorNode.isHidden = false @@ -318,10 +437,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { if let backgroundContent = self.backgroundContent { transition.updateFrame(node: backgroundContent, frame: bounds) if let (rect, containerSize) = self.absolutePosition { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + self.update(rect: rect, within: containerSize) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b09798b91b..7821224342 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -2673,40 +2673,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, transition: ContainedViewLayoutTransition) { - guard let _ = self.item, let animationNode = self.animationNode else { + func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + guard let item = self.item else { return } - let targetFrame = self.view.convert(animationNode.frame, to: messageContainer.parentView) - messageContainer.animateBackgroundFrame(to: targetFrame, transition: transition) - - var backgroundFrame = messageContainer.frame - backgroundFrame.size.width = backgroundFrame.size.height - -// let localSourceContentFrame = self.contextSourceNode.contentNode.view.convert(backgroundFrame.offsetBy(dx: self.contextSourceNode.contentRect.minX, dy: self.contextSourceNode.contentRect.minY), to: self.contextSourceNode.contentNode.view) - -// let sourceCenter = CGPoint( -// x: localSourceContentFrame.minX + 11.2, -// y: localSourceContentFrame.midY - 1.8 -// ) - - let sourceScale: CGFloat = 36.0 / self.imageNode.frame.height - - let offset = CGPoint(x: -90.0, y: 90.0) -// let offset = CGPoint( -// x: sourceCenter.x - self.imageNode.frame.midX, -// y: sourceCenter.y - self.imageNode.frame.midY -// ) - - transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) - transition.animateTransformScale(node: self.imageNode, from: sourceScale) - if let animationNode = self.animationNode { - transition.animatePositionAdditive(layer: animationNode.layer, offset: offset) - transition.animateTransformScale(node: animationNode, from: sourceScale) - } - transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) - transition.animateTransformScale(node: self.placeholderNode, from: sourceScale) + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + transition.animatePositionAdditive(node: self, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0), delay: delay) + transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } override func openMessageContextMenu() { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 86607990e5..b23bb31840 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -797,46 +797,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, transition: ContainedViewLayoutTransition) { - guard let _ = self.item else { + func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + guard let item = self.item else { return } - - let targetFrame = self.view.convert(self.backgroundNode.frame, to: messageContainer.parentView) - messageContainer.animateBackgroundFrame(to: targetFrame, transition: transition) - -// let backgroundFrame = messageContainer.frame -//// let widthDifference = self.backgroundNode.frame.width - backgroundFrame.width -//// let heightDifference = self.backgroundNode.frame.height - backgroundFrame.height -// -// if let type = self.backgroundNode.type { -// if case .none = type { -// } else { -// self.clippingNode.clipsToBounds = true -// } -// } -// transition.animateFrame(layer: self.clippingNode.layer, from: CGRect(origin: CGPoint(x: self.clippingNode.frame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: { [weak self] _ in -// guard let strongSelf = self else { -// return -// } -// strongSelf.clippingNode.clipsToBounds = false -// }) -// transition.animateOffsetAdditive(layer: self.clippingNode.layer, offset: backgroundFrame.minY - self.clippingNode.frame.minY) -// -// let combinedTransition = CombinedTransition(horizontal: transition, vertical: transition) -// -// self.backgroundWallpaperNode.animateFrom(sourceView: messageContainer.bubbleNode.view, mediaBox: item.context.account.postbox.mediaBox, transition: combinedTransition) -// self.backgroundNode.animateFrom(sourceView: messageContainer.bubbleNode.view, transition: combinedTransition) - -// for contentNode in self.contentNodes { -// if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { -// let localSourceContentFrame = self.mainContextSourceNode.contentNode.view.convert(textInput.contentView.frame.offsetBy(dx: self.mainContextSourceNode.contentRect.minX, dy: self.mainContextSourceNode.contentRect.minY), to: contentNode.view) -// textInput.contentView.frame = localSourceContentFrame -// contentNode.animateFrom(sourceView: textInput.contentView, scrollOffset: textInput.scrollOffset, widthDifference: widthDifference, transition: transition) -// } else if let contentNode = contentNode as? ChatMessageWebpageBubbleContentNode { -// transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: 0.0, y: heightDifference)) -// } -// } + + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + transition.animatePositionAdditive(node: self, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0), delay: delay) + transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 2914f4bf43..9cfbbc2c6c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -1165,6 +1165,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + guard let item = self.item else { + return + } + + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + transition.animatePositionAdditive(node: self, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0), delay: delay) + transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) + } + func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { snapshotView.frame = self.interactiveVideoNode.view.convert(snapshotView.frame, from: self.contextSourceNode.contentNode.view) self.interactiveVideoNode.animateFromSnapshot(snapshotView: snapshotView, transition: transition) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 308ee6f10e..c5d88c0204 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -1641,6 +1641,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + guard let item = self.item else { + return + } + + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + transition.animatePositionAdditive(node: self, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0), delay: delay) + transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) + } + override func openMessageContextMenu() { guard let item = self.item else { return