diff --git a/submodules/MessageReactionListUI/MessageReactionListUI.xcodeproj/project.pbxproj b/submodules/MessageReactionListUI/MessageReactionListUI.xcodeproj/project.pbxproj index 436c007781..4343579642 100644 --- a/submodules/MessageReactionListUI/MessageReactionListUI.xcodeproj/project.pbxproj +++ b/submodules/MessageReactionListUI/MessageReactionListUI.xcodeproj/project.pbxproj @@ -587,6 +587,17 @@ sourceTree SOURCE_ROOT + 1DD70E29B0C2D20A00000000 + + isa + PBXFileReference + name + MessageReactionListLoadingPlaceholder.swift + path + Sources/MessageReactionListLoadingPlaceholder.swift + sourceTree + SOURCE_ROOT + B401C979EAB5339800000000 isa @@ -599,6 +610,7 @@ 1DD70E298DEA121500000000 1DD70E29C02D1D4F00000000 + 1DD70E29B0C2D20A00000000 B401C979639FD30200000000 @@ -671,6 +683,13 @@ fileRef 1DD70E29C02D1D4F00000000 + E7A30F04B0C2D20A00000000 + + isa + PBXBuildFile + fileRef + 1DD70E29B0C2D20A00000000 + 1870857F0000000000000000 isa @@ -679,6 +698,7 @@ E7A30F048DEA121500000000 E7A30F04C02D1D4F00000000 + E7A30F04B0C2D20A00000000 E7A30F04D65BA68200000000 diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift index edcd28fd0e..40c7c8063d 100644 --- a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift +++ b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift @@ -11,7 +11,7 @@ import ItemListPeerItem public final class MessageReactionListController: ViewController { private let context: AccountContext private let messageId: MessageId - private let presentatonData: PresentationData + private let presentationData: PresentationData private let initialReactions: [MessageReaction] private var controllerNode: MessageReactionListControllerNode { @@ -28,7 +28,7 @@ public final class MessageReactionListController: ViewController { public init(context: AccountContext, messageId: MessageId, initialReactions: [MessageReaction]) { self.context = context self.messageId = messageId - self.presentatonData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.initialReactions = initialReactions super.init(navigationBarPresentationData: nil) @@ -41,7 +41,7 @@ public final class MessageReactionListController: ViewController { } override public func loadDisplayNode() { - self.displayNode = MessageReactionListControllerNode(context: self.context, presentatonData: self.presentatonData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in + self.displayNode = MessageReactionListControllerNode(context: self.context, presentationData: self.presentationData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in self?.dismiss() }) @@ -113,14 +113,14 @@ private let itemHeight: CGFloat = 50.0 private func topInsetForLayout(layout: ContainerViewLayout, itemCount: Int) -> CGFloat { let contentHeight = CGFloat(itemCount) * itemHeight - let minimumItemHeights: CGFloat = contentHeight + let minimumItemHeights: CGFloat = max(contentHeight, itemHeight * 5.0) return max(layout.size.height - layout.intrinsicInsets.bottom - minimumItemHeights, headerHeight) } private final class MessageReactionListControllerNode: ViewControllerTracingNode { private let context: AccountContext - private let presentatonData: PresentationData + private let presentationData: PresentationData private let dismiss: () -> Void private let listContext: MessageReactionListContext @@ -129,9 +129,12 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode private let backgroundNode: ASDisplayNode private let contentHeaderContainerNode: ASDisplayNode private let contentHeaderContainerBackgroundNode: ASImageNode + private let contentHeaderContainerSeparatorNode: ASDisplayNode private var categoryItemNodes: [MessageReactionCategoryNode] = [] private let categoryScrollNode: ASScrollNode private let listNode: ListView + private var placeholderNode: MessageReactionListLoadingPlaceholder? + private var placeholderNodeIsAnimatingOut = false private var validLayout: ContainerViewLayout? @@ -146,26 +149,29 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode private var forceHeaderTransition: ContainedViewLayoutTransition? - init(context: AccountContext, presentatonData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) { + init(context: AccountContext, presentationData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) { self.context = context - self.presentatonData = presentatonData + self.presentationData = presentationData self.dismiss = dismiss self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.presentatonData.theme.actionSheet.opaqueItemBackgroundColor + self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor self.contentHeaderContainerNode = ASDisplayNode() self.contentHeaderContainerBackgroundNode = ASImageNode() self.contentHeaderContainerBackgroundNode.displaysAsynchronously = false + self.contentHeaderContainerSeparatorNode = ASDisplayNode() + self.contentHeaderContainerSeparatorNode.backgroundColor = self.presentationData.theme.list.itemPlainSeparatorColor + self.categoryScrollNode = ASScrollNode() self.contentHeaderContainerBackgroundNode.displayWithoutProcessing = true self.contentHeaderContainerBackgroundNode.image = generateImage(CGSize(width: 10.0, height: 10.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentatonData.theme.rootController.navigationBar.backgroundColor.cgColor) + context.setFillColor(presentationData.theme.rootController.navigationBar.backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width, height: size.height / 2.0))) })?.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) @@ -173,6 +179,9 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode self.listNode = ListView() self.listNode.limitHitTestToNodes = true + self.placeholderNode = MessageReactionListLoadingPlaceholder(theme: presentationData.theme, itemHeight: itemHeight) + self.placeholderNode?.isUserInteractionEnabled = false + self.listContext = MessageReactionListContext(postbox: self.context.account.postbox, network: self.context.account.network, messageId: messageId, initialReactions: initialReactions) super.init() @@ -182,9 +191,11 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode self.listNode.stackFromBottom = false self.addSubnode(self.listNode) + self.placeholderNode.flatMap(self.addSubnode) self.addSubnode(self.contentHeaderContainerNode) self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerBackgroundNode) + self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerSeparatorNode) self.contentHeaderContainerNode.addSubnode(self.categoryScrollNode) self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in @@ -198,6 +209,10 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode let topOffset = offset transition.updateFrame(node: strongSelf.contentHeaderContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight), size: CGSize(width: layout.size.width, height: headerHeight))) transition.updateFrame(node: strongSelf.contentHeaderContainerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: headerHeight))) + transition.updateFrame(node: strongSelf.contentHeaderContainerSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + if let placeholderNode = strongSelf.placeholderNode { + transition.updateFrame(node: placeholderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: placeholderNode.bounds.size)) + } transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight / 2.0), size: CGSize(width: layout.size.width, height: layout.size.height + 300.0))) } @@ -223,11 +238,8 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - //transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) - //transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) - - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) + transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) var currentCategoryItemCount = 0 if let currentState = self.currentState { @@ -243,6 +255,12 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount) insets.bottom = layout.intrinsicInsets.bottom + if let placeholderNode = self.placeholderNode, !self.placeholderNodeIsAnimatingOut { + let placeholderHeight = min(CGFloat(currentCategoryItemCount) * itemHeight, layout.size.height) + UIScreenPixel + placeholderNode.frame = CGRect(origin: placeholderNode.frame.origin, size: CGSize(width: layout.size.width, height: placeholderHeight)) + placeholderNode.updateLayout(size: CGSize(width: layout.size.width, height: placeholderHeight)) + } + var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -322,7 +340,7 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode let states = self.currentState?.states ?? [] for (category, categoryState) in states { if self.categoryItemNodes.count <= index { - let itemNode = MessageReactionCategoryNode(theme: self.presentatonData.theme, category: category, count: categoryState.count, action: { [weak self] in + let itemNode = MessageReactionCategoryNode(theme: self.presentationData.theme, category: category, count: categoryState.count, action: { [weak self] in self?.setCategory(category) }) self.categoryItemNodes.append(itemNode) @@ -341,7 +359,7 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode } index += 1 } - let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentatonData) + let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentationData) let previousWasEmpty = self.currentEntries == nil || self.currentEntries?.count == 0 let isEmpty = entries.isEmpty self.currentEntries = entries @@ -350,7 +368,20 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode self.dequeueTransaction() if previousWasEmpty && !isEmpty { - self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + if let placeholderNode = self.placeholderNode { + self.placeholderNodeIsAnimatingOut = true + placeholderNode.allowsGroupOpacity = true + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.placeholderNode?.removeFromSupernode() + strongSelf.placeholderNode = nil + }) + } + self.listNode.forEachItemNode({ itemNode in + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + }) } } @@ -380,9 +411,8 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode var options = ListViewDeleteAndInsertOptions() options.insert(.Synchronous) - //options.insert(.AnimateTopItemPosition) - //options.insert(.AnimateCrossfade) options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) var currentCategoryItemCount = 0 if let currentState = self.currentState { @@ -417,6 +447,15 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode return result } } - return super.hitTest(point, with: event) + if let result = self.listNode.hitTest(self.view.convert(point, to: self.listNode.view), with: event) { + return result + } + if point.y >= self.contentHeaderContainerNode.frame.minY && point.y < self.bounds.height { + return self.listNode.view + } + if point.y >= 0 && point.y < self.contentHeaderContainerNode.frame.minY { + return self.dimNode.view + } + return nil } } diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift new file mode 100644 index 0000000000..676947eb9c --- /dev/null +++ b/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift @@ -0,0 +1,80 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TelegramCore + +final class MessageReactionListLoadingPlaceholder: ASDisplayNode { + private let theme: PresentationTheme + private let itemHeight: CGFloat + private let itemImage: UIImage? + + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let highlightNode: ASImageNode + private var itemNodes: [ASImageNode] = [] + + init(theme: PresentationTheme, itemHeight: CGFloat) { + self.theme = theme + self.itemHeight = itemHeight + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = UIColor(white: 0.92, alpha: 1.0) + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor + + self.highlightNode = ASImageNode() + self.highlightNode.displaysAsynchronously = false + self.highlightNode.displayWithoutProcessing = true + + let leftInset: CGFloat = 15.0 + let avatarSize: CGFloat = 40.0 + let avatarSpacing: CGFloat = 11.0 + let contentWidth: CGFloat = 4.0 + let contentHeight: CGFloat = 14.0 + let rightInset: CGFloat = 54.0 + self.itemImage = generateImage(CGSize(width: leftInset + avatarSize + avatarSpacing + contentWidth + rightInset, height: itemHeight), rotatedContext: { size, context in + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.itemPlainSeparatorColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel))) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: leftInset, y: floor((itemHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + let contentOrigin = leftInset + avatarSize + avatarSpacing + context.fill(CGRect(origin: CGPoint(x: contentOrigin, y: floor((size.height - contentHeight) / 2.0)), size: CGSize(width: size.width - contentOrigin - rightInset, height: contentHeight))) + })?.stretchableImage(withLeftCapWidth: Int(leftInset + avatarSize + avatarSpacing + 1), topCapHeight: 0) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightNode) + self.addSubnode(self.separatorNode) + } + + func updateLayout(size: CGSize) { + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + + var verticalOffset: CGFloat = 0.0 + var index = 0 + while verticalOffset < size.height - 1.0 { + if self.itemNodes.count >= index { + let itemNode = ASImageNode() + itemNode.image = self.itemImage + self.itemNodes.append(itemNode) + self.addSubnode(itemNode) + } + self.itemNodes[index].frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: self.itemHeight)) + verticalOffset += self.itemHeight + index += 1 + } + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: UIScreenPixel)) + if index < self.itemNodes.count { + for i in index ..< self.itemNodes.count { + self.itemNodes[i].removeFromSupernode() + } + self.itemNodes.removeLast(self.itemNodes.count - index) + } + } +} diff --git a/submodules/ReactionSelectionNode/ReactionSelectionNode.xcodeproj/project.pbxproj b/submodules/ReactionSelectionNode/ReactionSelectionNode.xcodeproj/project.pbxproj index b61bbac9d2..5e0a81a077 100644 --- a/submodules/ReactionSelectionNode/ReactionSelectionNode.xcodeproj/project.pbxproj +++ b/submodules/ReactionSelectionNode/ReactionSelectionNode.xcodeproj/project.pbxproj @@ -283,6 +283,17 @@ explicitFileType text.script.python + 1DD70E2982B4C61400000000 + + isa + PBXFileReference + name + ReactionAttachedNode.swift + path + Sources/ReactionAttachedNode.swift + sourceTree + SOURCE_ROOT + 1DD70E29CCCE0EED00000000 isa @@ -348,6 +359,7 @@ ]]> children + 1DD70E2982B4C61400000000 1DD70E29CCCE0EED00000000 1DD70E2930A5FA5800000000 1DD70E29691388CA00000000 @@ -385,6 +397,13 @@ B401C979E09F1DE500000000 + E7A30F0482B4C61400000000 + + isa + PBXBuildFile + fileRef + 1DD70E2982B4C61400000000 + E7A30F04CCCE0EED00000000 isa @@ -426,6 +445,7 @@ PBXSourcesBuildPhase files + E7A30F0482B4C61400000000 E7A30F04CCCE0EED00000000 E7A30F0430A5FA5800000000 E7A30F04691388CA00000000 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionAttachedNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionAttachedNode.swift new file mode 100644 index 0000000000..466a1bed6f --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionAttachedNode.swift @@ -0,0 +1,322 @@ +import Foundation +import AsyncDisplayKit +import AnimationUI +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import AppBundle + +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +final class ReactionAttachedNode: ASDisplayNode { + private let account: Account + private let theme: PresentationTheme + private let reactions: [ReactionGestureItem] + + private let backgroundNode: ASImageNode + private let backgroundShadowNode: ASImageNode + private let bubbleNodes: [(ASImageNode, ASImageNode)] + private var reactionNodes: [ReactionNode] = [] + private var hasSelectedNode = false + + private let hapticFeedback = HapticFeedback() + + private var shadowBlur: CGFloat = 8.0 + private var minimizedReactionSize: CGFloat = 30.0 + private var maximizedReactionSize: CGFloat = 60.0 + private var smallCircleSize: CGFloat = 8.0 + + public init(account: Account, theme: PresentationTheme, reactions: [ReactionGestureItem]) { + self.account = account + self.theme = theme + self.reactions = reactions + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.backgroundShadowNode = ASImageNode() + self.backgroundShadowNode.displaysAsynchronously = false + self.backgroundShadowNode.displayWithoutProcessing = true + + self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in + let imageNode = ASImageNode() + imageNode.displaysAsynchronously = false + imageNode.displayWithoutProcessing = true + + let shadowNode = ASImageNode() + shadowNode.displaysAsynchronously = false + shadowNode.displayWithoutProcessing = true + + return (imageNode, shadowNode) + } + + super.init() + + self.bubbleNodes.forEach { _, shadow in + self.addSubnode(shadow) + } + self.addSubnode(self.backgroundShadowNode) + self.bubbleNodes.forEach { foreground, _ in + self.addSubnode(foreground) + } + self.addSubnode(self.backgroundNode) + } + + func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) { + let initialAnchorX = startingPoint.x + + if isInitial && self.reactionNodes.isEmpty { + let availableContentWidth = constrainedSize.width + var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2) + minimizedReactionSize = max(16.0, floor(minimizedReactionSize)) + minimizedReactionSize = min(30.0, minimizedReactionSize) + + self.minimizedReactionSize = minimizedReactionSize + self.shadowBlur = floor(minimizedReactionSize * 0.26) + self.smallCircleSize = 8.0 + + let backgroundHeight = floor(minimizedReactionSize * 1.4) + + self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur) + self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur) + for i in 0 ..< self.bubbleNodes.count { + self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + } + + self.reactionNodes = self.reactions.map { reaction -> ReactionNode in + return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize, loadFirstFrame: true) + } + self.reactionNodes.forEach(self.addSubnode(_:)) + } + + let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.4) + + let reactionSpacing: CGFloat = floor(self.minimizedReactionSize * 0.2) + let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) + + let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing + + var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0)) + backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + backgroundFrame.origin.x = max(0.0, backgroundFrame.minX) + backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX) + + self.backgroundNode.frame = backgroundFrame + self.backgroundShadowNode.frame = backgroundFrame + + let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0 + let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0 + let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) + + var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing + if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX { + self.hasSelectedNode = false + } else { + self.hasSelectedNode = true + } + + var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) + maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) + + for iterationIndex in 0 ..< self.reactionNodes.count { + var i = iterationIndex + let isMaximized = i == maximizedIndex + + let reactionSize: CGFloat + if isMaximized { + reactionSize = maximizedReactionSize + } else { + reactionSize = minimizedReactionSize + } + + let transition: ContainedViewLayoutTransition + if isInitial { + transition = .immediate + } else { + transition = .animated(duration: 0.18, curve: .easeInOut) + } + + if self.reactionNodes[i].isMaximized != isMaximized { + self.reactionNodes[i].isMaximized = isMaximized + self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial) + if isMaximized && !isInitial { + self.hapticFeedback.tap() + } + } + + var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize)) + if isMaximized { + reactionFrame.origin.x -= 9.0 + reactionFrame.size.width += 18.0 + } + self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 18.0), transition: transition, displayText: isMaximized) + + transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true) + + reactionX += reactionSize + reactionSpacing + } + + let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0)) + self.bubbleNodes[1].0.frame = mainBubbleFrame + self.bubbleNodes[1].1.frame = mainBubbleFrame + + let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0)) + self.bubbleNodes[0].0.frame = secondaryBubbleFrame + self.bubbleNodes[0].1.frame = secondaryBubbleFrame + } + + func animateIn() { + self.bubbleNodes[1].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.bubbleNodes[1].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + + self.bubbleNodes[0].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.bubbleNodes[0].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + + let backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0) + let damping: CGFloat = 100.0 + + for i in 0 ..< self.reactionNodes.count { + let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1) + var nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + nodeOffset.x = -nodeOffset.x + nodeOffset.y = 30.0 + self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.28, initialVelocity: 0.0, damping: damping) + self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + } + + self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) + self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + self.backgroundShadowNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) + self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + } + + func animateOut(into targetNode: ASImageNode?, hideTarget: Bool, completion: @escaping () -> Void) { + self.hapticFeedback.prepareTap() + + var completedContainer = false + var completedTarget = true + + let intermediateCompletion: () -> Void = { + if completedContainer && completedTarget { + completion() + } + } + + if let targetNode = targetNode { + for i in 0 ..< self.reactionNodes.count { + if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { + if let snapshotView = self.reactionNodes[i].view.snapshotContentTree() { + let targetSnapshotView = UIImageView() + targetSnapshotView.image = targetNode.image + targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) + self.reactionNodes[i].isHidden = true + self.view.addSubview(targetSnapshotView) + self.view.addSubview(snapshotView) + completedTarget = false + let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view) + let duration: Double = 0.3 + if hideTarget { + targetNode.isHidden = true + } + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false) + + + let sourcePoint = snapshotView.center + let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0) + + let x1 = sourcePoint.x + let y1 = sourcePoint.y + let x2 = midPoint.x + let y2 = midPoint.y + let x3 = targetPosition.x + let y3 = targetPosition.y + + let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + + var keyframes: [AnyObject] = [] + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = a * x * x + b * x + c + keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) + } + + snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.hapticFeedback.tap() + } + completedTarget = true + if hideTarget { + targetNode.isHidden = false + targetNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0) + } + intermediateCompletion() + }) + targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false) + + snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false) + } + break + } + } + } + + self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completedContainer = true + intermediateCompletion() + }) + for (node, shadow) in self.bubbleNodes { + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + shadow.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + shadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + for i in 0 ..< self.reactionNodes.count { + self.reactionNodes[i].layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.reactionNodes[i].layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + } + + func selectedReaction() -> ReactionGestureItem? { + if !self.hasSelectedNode { + return nil + } + for i in 0 ..< self.reactionNodes.count { + if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { + return self.reactionNodes[i].reaction + } + } + return nil + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 8ebb44a1f8..6a3be9ecf7 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -227,16 +227,16 @@ final class ReactionSelectionNode: ASDisplayNode { super.init() self.bubbleNodes.forEach { _, shadow in - self.addSubnode(shadow) + //self.addSubnode(shadow) } self.addSubnode(self.backgroundShadowNode) self.bubbleNodes.forEach { foreground, _ in - self.addSubnode(foreground) + //self.addSubnode(foreground) } self.addSubnode(self.backgroundNode) } - func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) { + func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool, touchPoint: CGPoint) { let initialAnchorX = startingPoint.x var isRightAligned = false @@ -285,23 +285,40 @@ final class ReactionSelectionNode: ASDisplayNode { backgroundFrame.origin.x = max(0.0, backgroundFrame.minX) backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX) - self.isRightAligned = isRightAligned - self.backgroundNode.frame = backgroundFrame - self.backgroundShadowNode.frame = backgroundFrame - let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0 let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0 let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) + var maximizedIndex = -1 + if let reaction = self.reactions.last, case .reply = reaction { + maximizedIndex = self.reactions.count - 1 + } + if backgroundFrame.insetBy(dx: -10.0, dy: -10.0).contains(touchPoint) { + maximizedIndex = Int(((touchPoint.x - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) + maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) + } + if maximizedIndex == -1 { + backgroundFrame.size.width -= maximizedReactionSize - minimizedReactionSize + backgroundFrame.origin.x += maximizedReactionSize - minimizedReactionSize + } + + self.isRightAligned = isRightAligned + + let backgroundTransition: ContainedViewLayoutTransition + if isInitial { + backgroundTransition = .immediate + } else { + backgroundTransition = .animated(duration: 0.18, curve: .easeInOut) + } + backgroundTransition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + backgroundTransition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame) + var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX { self.hasSelectedNode = false } else { self.hasSelectedNode = true } - - var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) - maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) for iterationIndex in 0 ..< self.reactionNodes.count { var i = iterationIndex @@ -484,9 +501,6 @@ final class ReactionSelectionNode: ASDisplayNode { } func selectedReaction() -> ReactionGestureItem? { - if !self.hasSelectedNode { - return nil - } for i in 0 ..< self.reactionNodes.count { if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { return self.reactionNodes[i].reaction diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift index 02719c5884..8290a9a4b1 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -10,7 +10,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode { private let theme: PresentationTheme private var currentNode: ReactionSelectionNode? - private var currentLocation: (CGPoint, CGFloat)? + private var currentLocation: (CGPoint, CGFloat, CGPoint)? private var validLayout: (size: CGSize, insets: UIEdgeInsets)? @@ -21,7 +21,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode { super.init() } - func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint) { + func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint, touchPoint: CGPoint) { if let currentNode = self.currentNode { currentNode.removeFromSupernode() self.currentNode = nil @@ -30,7 +30,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode { let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions) self.addSubnode(reactionNode) self.currentNode = reactionNode - self.currentLocation = (point, point.x) + self.currentLocation = (point, point.x, touchPoint) if let (size, insets) = self.validLayout { self.update(size: size, insets: insets, isInitial: true) @@ -55,9 +55,9 @@ public final class ReactionSelectionParentNode: ASDisplayNode { } } - func updateReactionsAnchor(point: CGPoint) { - if let (currentPoint, _) = self.currentLocation { - self.currentLocation = (currentPoint, point.x) + func updateReactionsAnchor(point: CGPoint, touchPoint: CGPoint) { + if let (currentPoint, _, _) = self.currentLocation { + self.currentLocation = (currentPoint, point.x, touchPoint) if let (size, insets) = self.validLayout { self.update(size: size, insets: insets, isInitial: false) @@ -72,8 +72,8 @@ public final class ReactionSelectionParentNode: ASDisplayNode { } private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) { - if let currentNode = self.currentNode, let (point, offset) = currentLocation { - currentNode.updateLayout(constrainedSize: size, startingPoint: point, offsetFromStart: offset, isInitial: isInitial) + if let currentNode = self.currentNode, let (point, offset, touchPoint) = self.currentLocation { + currentNode.updateLayout(constrainedSize: size, startingPoint: CGPoint(x: size.width - 32.0, y: point.y), offsetFromStart: offset, isInitial: isInitial, touchPoint: touchPoint) currentNode.frame = CGRect(origin: CGPoint(), size: size) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift index ceb67815f1..f82c3df64f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift @@ -15,12 +15,16 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { public var availableReactions: (() -> [ReactionGestureItem])? public var getReactionContainer: (() -> ReactionSelectionParentNode?)? + public var getAnchorPoint: (() -> CGPoint?)? public var began: (() -> Void)? public var updateOffset: ((CGFloat, Bool) -> Void)? public var completed: ((ReactionGestureItem?) -> Void)? public var displayReply: ((CGFloat) -> Void)? public var activateReply: (() -> Void)? + private var currentAnchorPoint: CGPoint? + private var currentAnchorStartPoint: CGPoint? + override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -95,17 +99,20 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.f() } } - let activationTimer = Timer(timeInterval: 0.3, target: TimerTarget { [weak self] in + let activationTimer = Timer(timeInterval: 0.1, target: TimerTarget { [weak self] in guard let strongSelf = self else { return } strongSelf.activationTimer = nil if strongSelf.validatedGesture { let location = strongSelf.currentLocation - if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?() { + if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?(), let localAnchorPoint = strongSelf.getAnchorPoint?() { strongSelf.currentContainer = reactionContainer - let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) - reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation) + let reactionContainerLocation = reactionContainer.view.convert(localAnchorPoint, from: strongSelf.view) + let reactionContainerTouchPoint = reactionContainer.view.convert(location, from: nil) + strongSelf.currentAnchorPoint = reactionContainerLocation + strongSelf.currentAnchorStartPoint = location + reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation, touchPoint: reactionContainerTouchPoint) } } }, selector: #selector(TimerTarget.event), userInfo: nil, repeats: false) @@ -124,9 +131,11 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.displayReply?(-min(0.0, translation.x)) } } else { - if let reactionContainer = self.currentContainer { - let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) - reactionContainer.updateReactionsAnchor(point: reactionContainerLocation) + if let reactionContainer = self.currentContainer, let currentAnchorPoint = self.currentAnchorPoint, let currentAnchorStartPoint = self.currentAnchorStartPoint { + let anchorPoint = CGPoint(x: currentAnchorPoint.x + location.x - currentAnchorStartPoint.x, y: currentAnchorPoint.y) + let reactionContainerLocation = anchorPoint + let reactionContainerTouchPoint = reactionContainer.view.convert(location, from: nil) + reactionContainer.updateReactionsAnchor(point: reactionContainerLocation, touchPoint: reactionContainerTouchPoint) } } super.touchesMoved(touches, with: event) diff --git a/submodules/TelegramCore/TelegramCore/MessageReactionList.swift b/submodules/TelegramCore/TelegramCore/MessageReactionList.swift index 85a6b34d8b..bbae3101af 100644 --- a/submodules/TelegramCore/TelegramCore/MessageReactionList.swift +++ b/submodules/TelegramCore/TelegramCore/MessageReactionList.swift @@ -93,7 +93,7 @@ private final class MessageReactionCategoryContext { } let messageId = self.messageId let offset = self.state.nextOffset - let request = self.postbox.transaction { transaction -> Api.InputPeer? in + var request = self.postbox.transaction { transaction -> Api.InputPeer? in let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) return inputPeer } @@ -107,6 +107,9 @@ private final class MessageReactionCategoryContext { return .generic } } + //#if DEBUG + request = request |> delay(1.0, queue: .mainQueue()) + //#endif self.loadingDisposable.set((request |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/TelegramUI/AppDelegate.swift b/submodules/TelegramUI/TelegramUI/AppDelegate.swift index c86dc53093..1f3a2fce50 100644 --- a/submodules/TelegramUI/TelegramUI/AppDelegate.swift +++ b/submodules/TelegramUI/TelegramUI/AppDelegate.swift @@ -1185,7 +1185,10 @@ final class SharedApplicationContext { BITHockeyManager.shared().configure(withIdentifier: hockeyAppId, delegate: self) BITHockeyManager.shared().crashManager.crashManagerStatus = .alwaysAsk BITHockeyManager.shared().start() + #if targetEnvironment(simulator) + #else BITHockeyManager.shared().authenticator.authenticateInstallation() + #endif } NotificationCenter.default.addObserver(forName: UIWindow.didBecomeHiddenNotification, object: nil, queue: nil, using: { notification in diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 77a896f042..269b64f0f3 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1667,6 +1667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if !initialReactions.isEmpty { + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root)) } }) diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 4fd2a80d66..b8c3db0ce6 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -1045,13 +1045,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) - let separatorOffset: CGFloat - if apparentInputBackgroundFrame.maxY >= layout.size.height { - separatorOffset = UIScreenPixel - } else { - separatorOffset = -UIScreenPixel - } - transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y + separatorOffset), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index 2a474aa1ae..087e68358e 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -428,7 +428,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } let reactions: [(String, String, String)] = [ - ("😒", "Sad", "sad"), + ("😔", "Sad", "sad"), ("😳", "Surprised", "surprised"), ("😂", "Fun", "lol"), ("👍", "Like", "thumbsup"), @@ -436,19 +436,25 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode ] var reactionItems: [ReactionGestureItem] = [] - for (value, text, name) in reactions { + for (value, text, name) in reactions.reversed() { if let path = getAppBundle().path(forResource: name, ofType: "tgs") { reactionItems.append(.reaction(value: value, text: text, path: path)) } } if item.controllerInteraction.canSetupReply(item.message) { - reactionItems.append(.reply) + //reactionItems.append(.reply) } return reactionItems } reactionRecognizer.getReactionContainer = { [weak self] in return self?.item?.controllerInteraction.reactionContainerNode() } + reactionRecognizer.getAnchorPoint = { [weak self] in + guard let strongSelf = self else { + return nil + } + return CGPoint(x: strongSelf.backgroundNode.frame.maxX, y: strongSelf.backgroundNode.frame.minY) + } reactionRecognizer.began = { [weak self] in guard let strongSelf = self, let item = strongSelf.item else { return @@ -497,7 +503,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode strongSelf.swipeToReplyFeedback = HapticFeedback() } strongSelf.swipeToReplyFeedback?.tap() - if strongSelf.swipeToReplyNode == nil, false { + if strongSelf.swipeToReplyNode == nil { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper)) strongSelf.swipeToReplyNode = swipeToReplyNode strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)