diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f74c0c5ab4..f8e8305c16 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -724,15 +724,18 @@ public struct StoryCameraTransitionIn { public weak var sourceView: UIView? public let sourceRect: CGRect public let sourceCornerRadius: CGFloat + public let useFillAnimation: Bool public init( sourceView: UIView, sourceRect: CGRect, - sourceCornerRadius: CGFloat + sourceCornerRadius: CGFloat, + useFillAnimation: Bool ) { self.sourceView = sourceView self.sourceRect = sourceRect self.sourceCornerRadius = sourceCornerRadius + self.useFillAnimation = useFillAnimation } } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 481d4cc0f5..7c89ba3ba3 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1022,7 +1022,7 @@ public protocol ChatController: ViewController { var parentController: ViewController? { get set } var customNavigationController: NavigationController? { get set } - var dismissPreviewing: (() -> Void)? { get set } + var dismissPreviewing: ((Bool) -> (() -> Void))? { get set } var purposefulAction: (() -> Void)? { get set } var stateUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 28421df850..d694b93de4 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -174,7 +174,7 @@ public final class BrowserBookmarksScreen: ViewController { }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 861079ac79..843d60fc0c 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1502,7 +1502,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.presentInGlobalOverlay(contextController) } } else { - var dismissPreviewingImpl: (() -> Void)? + var dismissPreviewingImpl: ((Bool) -> (() -> Void))? let source: ContextContentSource if let location = location { source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) @@ -1510,8 +1510,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.customNavigationController = strongSelf.navigationController as? NavigationController chatController.canReadHistory.set(false) - chatController.dismissPreviewing = { - dismissPreviewingImpl?() + chatController.dismissPreviewing = { animateIn in + return dismissPreviewingImpl?(animateIn) ?? {} } source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } @@ -1519,8 +1519,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let contextController = ContextController(context: strongSelf.context, 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) - dismissPreviewingImpl = { [weak contextController] in - contextController?.dismiss() + dismissPreviewingImpl = { [weak self, weak contextController] animateIn in + if let self, let contextController { + if animateIn { + contextController.statusBar.statusBarStyle = .Ignore + self.present(contextController, in: .window(.root)) + return { + contextController.dismissNow() + } + } else { + contextController.dismiss() + } + } + return {} } } case let .forum(pinnedIndex, _, threadId, _, _): @@ -2794,7 +2805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private weak var storyCameraTooltip: TooltipScreen? - fileprivate func openStoryCamera(fromList: Bool) { + fileprivate func openStoryCamera(fromList: Bool, gesturePullOffset: CGFloat? = nil) { guard !self.context.isFrozen else { let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) self.push(controller) @@ -2920,8 +2931,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { cameraTransitionIn = StoryCameraTransitionIn( sourceView: transitionView, - sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 + sourceRect: gesturePullOffset.flatMap({ transitionView.bounds.offsetBy(dx: -$0, dy: 0) }) ?? transitionView.bounds, + sourceCornerRadius: transitionView.bounds.height * 0.5, + useFillAnimation: gesturePullOffset != nil ) } } else { @@ -2929,7 +2941,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController cameraTransitionIn = StoryCameraTransitionIn( sourceView: rightButtonView, sourceRect: rightButtonView.bounds, - sourceCornerRadius: rightButtonView.bounds.height * 0.5 + sourceCornerRadius: rightButtonView.bounds.height * 0.5, + useFillAnimation: false ) } } @@ -2983,6 +2996,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if case .chatList = self.location, let componentView = self.chatListHeaderView() { + componentView.storyComposeAction = { [weak self] offset in + guard let self else { + return + } + self.openStoryCamera(fromList: true, gesturePullOffset: offset) + } + componentView.storyPeerAction = { [weak self] peer in guard let self else { return diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 5e1c2fe0aa..58711d3b14 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2184,6 +2184,7 @@ public protocol ContextExtractedContentSource: AnyObject { var adjustContentHorizontally: Bool { get } var adjustContentForSideInset: Bool { get } var ignoreContentTouches: Bool { get } + var keepDefaultContentTouches: Bool { get } var blurBackground: Bool { get } var shouldBeDismissed: Signal { get } @@ -2217,6 +2218,10 @@ public extension ContextExtractedContentSource { var shouldBeDismissed: Signal { return .single(false) } + + var keepDefaultContentTouches: Bool { + return false + } } public final class ContextControllerTakeControllerInfo { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index e5f05efef2..5cd59d6571 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -181,6 +181,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size)) + guard self.controller.navigationController == nil else { + return + } self.controller.containerLayoutUpdated( ContainerViewLayout( size: size, @@ -376,7 +379,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let result = contentNode.containingItem.customHitTest?(contentPoint) { return result } else if let result = contentNode.containingItem.contentHitTest(contentPoint, with: event) { - if result is TextSelectionNodeView { + if source.keepDefaultContentTouches { + return result + } else if result is TextSelectionNodeView { return result } else if contentNode.containingItem.contentRect.contains(contentPoint) { return contentNode.containingItem.contentView diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 4a6be111a9..8a64406ecd 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -3336,7 +3336,8 @@ public func stickerMediaPickerController( transitionIn: CameraScreenImpl.TransitionIn( sourceView: cameraHolder.parentView, sourceRect: cameraHolder.parentView.bounds, - sourceCornerRadius: 0.0 + sourceCornerRadius: 0.0, + useFillAnimation: false ), transitionOut: { _ in return CameraScreenImpl.TransitionOut( @@ -3453,7 +3454,8 @@ public func avatarMediaPickerController( transitionIn: CameraScreenImpl.TransitionIn( sourceView: cameraHolder.parentView, sourceRect: cameraHolder.parentView.bounds, - sourceCornerRadius: 0.0 + sourceCornerRadius: 0.0, + useFillAnimation: false ), transitionOut: { _ in return CameraScreenImpl.TransitionOut( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index e33f04f5ef..7cb3c562a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -650,6 +650,9 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 19)) != 0 { flags.insert(.isPaidMessage) } + if (apiFlags & (1 << 21)) != 0 { + flags.insert(.isBusinessTransfer) + } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod @@ -702,6 +705,7 @@ public final class StarsContext { public static let isReaction = Flags(rawValue: 1 << 5) public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6) public static let isPaidMessage = Flags(rawValue: 1 << 7) + public static let isBusinessTransfer = Flags(rawValue: 1 << 8) } public enum Peer: Equatable { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 9886fb1adb..f3cec6741b 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1668,15 +1668,18 @@ public class CameraScreenImpl: ViewController, CameraScreen { public weak var sourceView: UIView? public let sourceRect: CGRect public let sourceCornerRadius: CGFloat + public let useFillAnimation: Bool public init( sourceView: UIView, sourceRect: CGRect, - sourceCornerRadius: CGFloat + sourceCornerRadius: CGFloat, + useFillAnimation: Bool ) { self.sourceView = sourceView self.sourceRect = sourceRect self.sourceCornerRadius = sourceCornerRadius + self.useFillAnimation = useFillAnimation } } @@ -2505,55 +2508,100 @@ public class CameraScreenImpl: ViewController, CameraScreen { if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView { let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) - if case .story = controller.mode { - let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width + if transitionIn.useFillAnimation { + self.backgroundView.alpha = 1.0 + self.backgroundView.layer.removeAllAnimations() - self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - self.requestUpdateLayout(hasAppeared: true, transition: .immediate) - }) - self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.transitionDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15) - let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height) - self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.previewContainerView.layer.animate( - from: minSide / 2.0 as NSNumber, - to: self.previewContainerView.layer.cornerRadius as NSNumber, - keyPath: "cornerRadius", - timingFunction: kCAMediaTimingFunctionSpring, - duration: 0.3 - ) - } else { - self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds - self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0) + let transitionMaskView = UIView() + transitionMaskView.frame = self.view.bounds + self.view.mask = transitionMaskView - self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0) + let transitionCircleLayer = SimpleShapeLayer() + transitionCircleLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 320.0, height: 320.0)), transform: nil) + transitionCircleLayer.fillColor = UIColor.white.cgColor + transitionCircleLayer.frame = CGSize(width: 320.0, height: 320.0).centered(in: sourceLocalFrame) + transitionMaskView.layer.addSublayer(transitionCircleLayer) - let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView) - let sourceCenter = sourceInnerFrame.center - self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - self.requestUpdateLayout(hasAppeared: true, transition: .immediate) - }) + let colorFillView = UIView() + colorFillView.backgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor + colorFillView.frame = self.view.bounds + colorFillView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.view.addSubview(colorFillView) - var sourceBounds = self.mainPreviewView.bounds - if let holder = controller.holder { - sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size)) + let iconLayer = SimpleLayer() + iconLayer.contents = generateAddIcon(color: self.presentationData.theme.list.itemCheckColors.foregroundColor)?.cgImage + iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)) + iconLayer.position = sourceLocalFrame.center + colorFillView.layer.addSublayer(iconLayer) + + let labelLayer = SimpleLayer() + if let image = generateAddLabel(strings: self.presentationData.strings, color: self.presentationData.theme.list.itemCheckColors.foregroundColor) { + labelLayer.contents = image.cgImage + labelLayer.bounds = CGRect(origin: .zero, size: image.size) + labelLayer.position = CGPoint(x: sourceLocalFrame.center.x, y: sourceLocalFrame.center.y + 43.0 - UIScreenPixel) + colorFillView.layer.addSublayer(labelLayer) } - self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height) - self.mainPreviewView.transform = CGAffineTransform.identity - self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - self.mainPreviewContainerView.addSubview(self.mainPreviewView) - Queue.mainQueue().justDispatch { - self.animatedIn = true - } + iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false) + labelLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false) + + transitionCircleLayer.animateScale(from: sourceLocalFrame.width / 320.0, to: 6.0, duration: 0.6, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + self.view.mask = nil + colorFillView.removeFromSuperview() }) - } - - if let view = self.componentHost.view { - view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + if case .story = controller.mode { + let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width + + self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + self.requestUpdateLayout(hasAppeared: true, transition: .immediate) + }) + self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height) + self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animate( + from: minSide / 2.0 as NSNumber, + to: self.previewContainerView.layer.cornerRadius as NSNumber, + keyPath: "cornerRadius", + timingFunction: kCAMediaTimingFunctionSpring, + duration: 0.3 + ) + } else { + self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds + self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0) + + self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0) + + let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView) + let sourceCenter = sourceInnerFrame.center + self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + self.requestUpdateLayout(hasAppeared: true, transition: .immediate) + }) + + var sourceBounds = self.mainPreviewView.bounds + if let holder = controller.holder { + sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size)) + } + self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height) + self.mainPreviewView.transform = CGAffineTransform.identity + self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + self.mainPreviewContainerView.addSubview(self.mainPreviewView) + Queue.mainQueue().justDispatch { + self.animatedIn = true + } + }) + } + + if let view = self.componentHost.view { + view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } } } @@ -3767,6 +3815,10 @@ public class CameraScreenImpl: ViewController, CameraScreen { self.dismiss(animated: false) } } + + public func animateIn() { + self.node.animateIn() + } public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { if let layout = self.validLayout, layout.metrics.isTablet { @@ -4009,3 +4061,37 @@ private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoi return position } + +private func generateAddIcon(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(3.0) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 15.0, y: 5.5)) + context.addLine(to: CGPoint(x: 15.0, y: 24.5)) + context.strokePath() + + context.move(to: CGPoint(x: 5.5, y: 15.0)) + context.addLine(to: CGPoint(x: 24.5, y: 15.0)) + context.strokePath() + }) +} + + +private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? { + let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center) + var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil) + textRect.size.width = ceil(textRect.size.width) + textRect.size.height = ceil(textRect.size.height) + + return generateImage(textRect.size, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + UIGraphicsPushContext(context) + titleString.draw(in: textRect) + UIGraphicsPopContext() + }) +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index d967016c40..dd2809fc38 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1480,6 +1480,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { updatedShareButtonNode.pressed = { [weak strongSelf] in strongSelf?.shareButtonPressed() } + updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in + strongSelf?.openQuickShare(node: node, gesture: gesture) + } } let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account) updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: !incoming ? updatedImageFrame.minX - buttonSize.width - 6.0 : updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0 + imageBottomPadding), size: buttonSize) @@ -2429,6 +2432,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) { + if let item = self.item { + item.controllerInteraction.displayQuickShare(item.message.id, node, gesture) + } + } + private var playedSwipeToReplyHaptic = false @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 8d43117c8c..181736089e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -5804,7 +5804,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) { if let item = self.item { - item.controllerInteraction.displayQuickShare(node, gesture) + item.controllerInteraction.displayQuickShare(item.message.id, node, gesture) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index e2d44addfe..587acc94ba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1012,6 +1012,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { updatedShareButtonNode.pressed = { [weak strongSelf] in strongSelf?.shareButtonPressed() } + updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in + strongSelf?.openQuickShare(node: node, gesture: gesture) + } } let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account) let shareButtonFrame = CGRect(origin: CGPoint(x: baseShareButtonFrame.minX, y: baseShareButtonFrame.maxY - buttonSize.height), size: buttonSize) @@ -1546,6 +1549,12 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) { + if let item = self.item { + item.controllerInteraction.displayQuickShare(item.message.id, node, gesture) + } + } + private var playedSwipeToReplyHaptic = false @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index b9dd7b8136..aa5f8341be 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -645,7 +645,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 813e2a7760..9e7d3345f7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -500,7 +500,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index c7beb06383..3ad695ce52 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -289,7 +289,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe backgroundSize.height += verticalInset let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += titleLayout.size.height backgroundSize.height += verticalSpacing @@ -297,7 +297,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe backgroundSize.height += subtitleLayout.size.height backgroundSize.height += verticalSpacing + paragraphSpacing - let infoConstrainedSize = CGSize(width: constrainedWidth * 0.7, height: CGFloat.greatestFiniteMagnitude) + let infoConstrainedSize = CGSize(width: floor(constrainedWidth * 0.7), height: CGFloat.greatestFiniteMagnitude) var maxTitleWidth: CGFloat = 0.0 var maxValueWidth: CGFloat = 0.0 @@ -389,7 +389,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe groupsValueLayoutAndApply = nil } - backgroundSize.width = horizontalContentInset * 2.0 + maxTitleWidth + attributeSpacing + maxValueWidth + backgroundSize.width = horizontalContentInset * 2.0 + max(titleLayout.size.width, maxTitleWidth + attributeSpacing + maxValueWidth) let disclaimerText: NSMutableAttributedString if let verification = item.verification { diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/BUILD b/submodules/TelegramUI/Components/Chat/QuickShareScreen/BUILD index 6b265a2672..c1ed545428 100644 --- a/submodules/TelegramUI/Components/Chat/QuickShareScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/AppBundle", "//submodules/PresentationDataUtils", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/AvatarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareScreen.swift b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareScreen.swift index e4ef4135b0..fb7fa471e6 100644 --- a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareScreen.swift +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareScreen.swift @@ -8,27 +8,38 @@ import TelegramCore import TextFormat import TelegramPresentationData import MultilineTextComponent -import LottieComponent import AccountContext import ViewControllerComponent import AvatarNode import ComponentDisplayAdapters +private let largeCircleSize: CGFloat = 16.0 +private let smallCircleSize: CGFloat = 8.0 + private final class QuickShareScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let sourceNode: ASDisplayNode let gesture: ContextGesture + let openPeer: (EnginePeer.Id) -> Void + let completion: (EnginePeer.Id) -> Void + let ready: Promise init( context: AccountContext, sourceNode: ASDisplayNode, - gesture: ContextGesture + gesture: ContextGesture, + openPeer: @escaping (EnginePeer.Id) -> Void, + completion: @escaping (EnginePeer.Id) -> Void, + ready: Promise ) { self.context = context self.sourceNode = sourceNode self.gesture = gesture + self.openPeer = openPeer + self.completion = completion + self.ready = ready } static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool { @@ -36,10 +47,16 @@ private final class QuickShareScreenComponent: Component { } final class View: UIView { + private let backgroundShadowLayer: SimpleLayer private let backgroundView: BlurredBackgroundView private let backgroundTintView: UIView private let containerView: UIView + private let largeCircleLayer: SimpleLayer + private let largeCircleShadowLayer: SimpleLayer + private let smallCircleLayer: SimpleLayer + private let smallCircleShadowLayer: SimpleLayer + private var items: [EnginePeer.Id: ComponentView] = [:] private var isUpdating: Bool = false @@ -55,12 +72,30 @@ private final class QuickShareScreenComponent: Component { private var initialContinueGesturePoint: CGPoint? private var didMoveFromInitialGesturePoint = false + private let hapticFeedback = HapticFeedback() + override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.backgroundView.clipsToBounds = true self.backgroundTintView = UIView() self.backgroundTintView.clipsToBounds = true + self.backgroundShadowLayer = SimpleLayer() + self.backgroundShadowLayer.opacity = 0.0 + + self.largeCircleLayer = SimpleLayer() + self.largeCircleShadowLayer = SimpleLayer() + self.smallCircleLayer = SimpleLayer() + self.smallCircleShadowLayer = SimpleLayer() + + self.largeCircleLayer.backgroundColor = UIColor.black.cgColor + self.largeCircleLayer.masksToBounds = true + self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0 + + self.smallCircleLayer.backgroundColor = UIColor.black.cgColor + self.smallCircleLayer.masksToBounds = true + self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0 + self.containerView = UIView() self.containerView.clipsToBounds = true @@ -68,6 +103,7 @@ private final class QuickShareScreenComponent: Component { self.addSubview(self.backgroundView) self.backgroundView.addSubview(self.backgroundTintView) + self.layer.addSublayer(self.backgroundShadowLayer) self.addSubview(self.containerView) } @@ -80,9 +116,40 @@ private final class QuickShareScreenComponent: Component { } func animateIn() { + self.hapticFeedback.impact() + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.animateBoundsSize(view: self.backgroundView, from: CGSize(width: 0.0, height: self.backgroundView.bounds.height), to: self.backgroundView.bounds.size) transition.animateBounds(view: self.containerView, from: CGRect(x: self.containerView.bounds.width / 2.0, y: 0.0, width: 0.0, height: self.backgroundView.bounds.height), to: self.containerView.bounds) + self.backgroundView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1) + self.backgroundTintView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundTintView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1) + + self.backgroundShadowLayer.opacity = 1.0 + transition.animateBoundsSize(layer: self.backgroundShadowLayer, from: CGSize(width: 0.0, height: self.backgroundShadowLayer.bounds.height), to: self.backgroundShadowLayer.bounds.size) + self.backgroundShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + let mainCircleDelay: Double = 0.01 + let backgroundCenter = self.backgroundView.frame.width / 2.0 + let backgroundWidth = self.backgroundView.frame.width + for item in self.items.values { + guard let itemView = item.view else { + continue + } + + let distance = abs(itemView.frame.center.x - backgroundCenter) + let distanceNorm = distance / backgroundWidth + let adjustedDistanceNorm = distanceNorm + let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3 + + itemView.isHidden = true + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemView] in + guard let itemView else { + return + } + itemView.isHidden = false + itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 0.63 as NSNumber, keyPath: "transform.scale", duration: 0.4) + }) + } Queue.mainQueue().after(0.3) { self.containerView.clipsToBounds = false @@ -98,21 +165,45 @@ private final class QuickShareScreenComponent: Component { } func highlightGestureMoved(location: CGPoint) { + var selectedPeerId: EnginePeer.Id? for (peerId, view) in self.items { guard let view = view.view else { continue } - if view.frame.contains(location) { - self.selectedPeerId = peerId - self.state?.updated(transition: .spring(duration: 0.3)) + if view.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) { + selectedPeerId = peerId break } } + if let selectedPeerId, selectedPeerId != self.selectedPeerId { + self.hapticFeedback.tap() + } + self.selectedPeerId = selectedPeerId + self.state?.updated(transition: .spring(duration: 0.3)) } func highlightGestureFinished(performAction: Bool) { if let selectedPeerId = self.selectedPeerId, performAction { - let _ = selectedPeerId + if let component = self.component, let peer = self.peers?.first(where: { $0.id == selectedPeerId }), let view = self.items[selectedPeerId]?.view as? ItemComponent.View, let controller = self.environment?.controller() { + controller.window?.forEachController({ controller in + if let controller = controller as? QuickShareToastScreen { + controller.dismiss() + } + }) + let toastScreen = QuickShareToastScreen( + context: component.context, + peer: peer, + sourceFrame: view.convert(view.bounds, to: nil), + action: { + component.openPeer(peer.id) + } + ) + controller.present(toastScreen, in: .window(.root)) + view.avatarNode.isHidden = true + + component.completion(peer.id) + } + self.animateOut { if let controller = self.environment?.controller() { controller.dismiss() @@ -158,6 +249,7 @@ private final class QuickShareScreenComponent: Component { } else { self.environment?.controller()?.dismiss() } + component.ready.set(.single(true)) }) component.gesture.externalUpdated = { [weak self] view, point in @@ -216,7 +308,19 @@ private final class QuickShareScreenComponent: Component { let padding: CGFloat = 5.0 let spacing: CGFloat = 7.0 let itemSize = CGSize(width: 38.0, height: 38.0) - let itemsCount = 5 + let selectedItemSize = CGSize(width: 60.0, height: 60.0) + let itemsCount = self.peers?.count ?? 5 + + let widthExtension: CGFloat = self.selectedPeerId != nil ? selectedItemSize.width - itemSize.width : 0.0 + + let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0 + widthExtension, height: itemSize.height + padding * 2.0) + let contentRect = CGRect( + origin: CGPoint( + x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)), + y: sourceRect.minY - size.height - padding * 2.0 + ), + size: size + ) var itemFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: itemSize) if let peers = self.peers { @@ -236,13 +340,18 @@ private final class QuickShareScreenComponent: Component { isFocused = peer.id == selectedPeerId } + let effectiveItemSize = isFocused == true ? selectedItemSize : itemSize + let effectiveItemFrame = CGRect(origin: itemFrame.origin.offsetBy(dx: 0.0, dy: itemSize.height - effectiveItemSize.height), size: effectiveItemSize) + let _ = componentView.update( transition: componentTransition, component: AnyComponent( ItemComponent( context: component.context, theme: environment.theme, + strings: environment.strings, peer: peer, + safeInsets: UIEdgeInsets(top: 0.0, left: contentRect.minX + effectiveItemFrame.minX, bottom: 0.0, right: availableSize.width - contentRect.maxX + contentRect.width - effectiveItemFrame.maxX), isFocused: isFocused ) ), @@ -253,29 +362,29 @@ private final class QuickShareScreenComponent: Component { if view.superview == nil { self.containerView.addSubview(view) } - componentTransition.setFrame(view: view, frame: itemFrame) + componentTransition.setScale(view: view, scale: effectiveItemSize.width / selectedItemSize.width) + componentTransition.setBounds(view: view, bounds: CGRect(origin: .zero, size: selectedItemSize)) + componentTransition.setPosition(view: view, position: effectiveItemFrame.center) } - itemFrame.origin.x += itemSize.width + spacing + itemFrame.origin.x += effectiveItemFrame.width + spacing } } - let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0, height: itemSize.height + padding * 2.0) - let contentRect = CGRect( - origin: CGPoint( - x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)), - y: sourceRect.minY - size.height - padding * 2.0 - ), - size: size - ) - self.containerView.layer.cornerRadius = size.height / 2.0 self.backgroundView.layer.cornerRadius = size.height / 2.0 self.backgroundTintView.layer.cornerRadius = size.height / 2.0 transition.setFrame(view: self.backgroundView, frame: contentRect) transition.setFrame(view: self.containerView, frame: contentRect) - self.backgroundView.update(size: contentRect.size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition) + self.backgroundView.update(size: contentRect.size, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundTintView, frame: CGRect(origin: .zero, size: contentRect.size)) + let shadowInset: CGFloat = 15.0 + let shadowColor = UIColor(white: 0.0, alpha: 0.4) + if self.backgroundShadowLayer.contents == nil, let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 46.0, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image) + } + transition.setFrame(layer: self.backgroundShadowLayer, frame: contentRect.insetBy(dx: -shadowInset, dy: -shadowInset)) + return availableSize } } @@ -293,17 +402,29 @@ public class QuickShareScreen: ViewControllerComponentContainer { private var processedDidAppear: Bool = false private var processedDidDisappear: Bool = false + private let readyValue = Promise() + override public var ready: Promise { + return self.readyValue + } + public init( context: AccountContext, sourceNode: ASDisplayNode, - gesture: ContextGesture + gesture: ContextGesture, + openPeer: @escaping (EnginePeer.Id) -> Void, + completion: @escaping (EnginePeer.Id) -> Void ) { + let componentReady = Promise() + super.init( context: context, component: QuickShareScreenComponent( context: context, sourceNode: sourceNode, - gesture: gesture + gesture: gesture, + openPeer: openPeer, + completion: completion, + ready: componentReady ), navigationBarAppearance: .none, statusBarStyle: .ignore, @@ -311,6 +432,8 @@ public class QuickShareScreen: ViewControllerComponentContainer { updatedPresentationData: nil ) self.navigationPresentation = .flatModal + + self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true))) } required public init(coder aDecoder: NSCoder) { @@ -360,18 +483,24 @@ public class QuickShareScreen: ViewControllerComponentContainer { private final class ItemComponent: Component { let context: AccountContext let theme: PresentationTheme + let strings: PresentationStrings let peer: EnginePeer + let safeInsets: UIEdgeInsets let isFocused: Bool? init( context: AccountContext, theme: PresentationTheme, + strings: PresentationStrings, peer: EnginePeer, + safeInsets: UIEdgeInsets, isFocused: Bool? ) { self.context = context self.theme = theme + self.strings = strings self.peer = peer + self.safeInsets = safeInsets self.isFocused = isFocused } @@ -379,6 +508,9 @@ private final class ItemComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.safeInsets != rhs.safeInsets { + return false + } if lhs.isFocused != rhs.isFocused { return false } @@ -386,7 +518,7 @@ private final class ItemComponent: Component { } final class View: UIView { - private let avatarNode: AvatarNode + fileprivate let avatarNode: AvatarNode private let backgroundNode: NavigationBackgroundNode private let text = ComponentView() @@ -415,11 +547,13 @@ private final class ItemComponent: Component { self.isUpdating = false } + let size = CGSize(width: 60.0, height: 60.0) + var title = component.peer.compactDisplayTitle var overrideImage: AvatarNodeImageOverride? if component.peer.id == component.context.account.peerId { overrideImage = .savedMessagesIcon - title = "Saved Messages" + title = component.strings.DialogList_SavedMessages } self.avatarNode.setPeer( @@ -430,22 +564,16 @@ private final class ItemComponent: Component { synchronousLoad: true ) - self.avatarNode.view.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) - self.avatarNode.view.bounds = CGRect(origin: .zero, size: availableSize) - self.avatarNode.updateSize(size: availableSize) + self.avatarNode.view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.avatarNode.view.bounds = CGRect(origin: .zero, size: size) + self.avatarNode.updateSize(size: size) - var scale: CGFloat = 1.0 - var alpha: CGFloat = 1.0 var textAlpha: CGFloat = 0.0 var textOffset: CGFloat = 6.0 if let isFocused = component.isFocused { - scale = isFocused ? 1.1 : 1.0 - alpha = isFocused ? 1.0 : 0.6 textAlpha = isFocused ? 1.0 : 0.0 textOffset = isFocused ? 0.0 : 6.0 } - transition.setScale(view: self.avatarNode.view, scale: scale) - transition.setAlpha(view: self.avatarNode.view, alpha: alpha) let textSize = self.text.update( transition: .immediate, @@ -459,10 +587,27 @@ private final class ItemComponent: Component { if textView.superview == nil { self.addSubview(textView) } - let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: -16.0 - textSize.height + textOffset), size: textSize) + + let initialX = floor((size.width - textSize.width) / 2.0) + var textFrame = CGRect(origin: CGPoint(x: initialX, y: -13.0 - textSize.height + textOffset), size: textSize) + + let sideInset: CGFloat = 8.0 + let textPadding: CGFloat = 8.0 + let leftDistanceToEdge = 0.0 - textFrame.minX + let rightDistanceToEdge = textFrame.maxX - size.width + + let leftSafeInset = component.safeInsets.left - textPadding - sideInset + let rightSafeInset = component.safeInsets.right - textPadding - sideInset + if leftSafeInset < leftDistanceToEdge { + textFrame.origin.x = -leftSafeInset + } + if rightSafeInset < rightDistanceToEdge { + textFrame.origin.x = size.width + rightSafeInset - textFrame.width + } + transition.setFrame(view: textView, frame: textFrame) - let backgroundFrame = textFrame.insetBy(dx: -7.0, dy: -3.0) + let backgroundFrame = textFrame.insetBy(dx: -textPadding, dy: -3.0 - UIScreenPixel) transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame) self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate) self.backgroundNode.updateColor(color: component.theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic, enableBlur: true, transition: .immediate) @@ -471,7 +616,7 @@ private final class ItemComponent: Component { transition.setAlpha(view: self.backgroundNode.view, alpha: textAlpha) } - return availableSize + return size } } @@ -483,3 +628,17 @@ private final class ItemComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +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(shadow.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(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0)) +} diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift new file mode 100644 index 0000000000..04d10d5eb6 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift @@ -0,0 +1,372 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle +import ViewControllerComponent +import AccountContext +import MultilineTextComponent +import AvatarNode +import Markdown +import LottieComponent + +private final class QuickShareToastScreenComponent: Component { + let context: AccountContext + let peer: EnginePeer + let sourceFrame: CGRect + let action: () -> Void + + init( + context: AccountContext, + peer: EnginePeer, + sourceFrame: CGRect, + action: @escaping () -> Void + ) { + self.context = context + self.peer = peer + self.sourceFrame = sourceFrame + self.action = action + } + + static func ==(lhs: QuickShareToastScreenComponent, rhs: QuickShareToastScreenComponent) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.sourceFrame != rhs.sourceFrame { + return false + } + return true + } + + final class View: UIView { + private let contentView: UIView + private let backgroundView: BlurredBackgroundView + + private let avatarNode: AvatarNode + private let animation = ComponentView() + + private let content = ComponentView() + + private var isUpdating: Bool = false + private var component: QuickShareToastScreenComponent? + private var environment: EnvironmentType? + private weak var state: EmptyComponentState? + + private var doneTimer: Foundation.Timer? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.contentView = UIView() + self.contentView.isUserInteractionEnabled = false + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.contentView) + self.contentView.addSubview(self.avatarNode.view) + + self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture))) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + deinit { + self.doneTimer?.invalidate() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.frame.contains(point) { + return nil + } + return super.hitTest(point, with: event) + } + + @objc private func tapGesture() { + guard let component = self.component else { + return + } + component.action() + self.environment?.controller()?.dismiss() + } + + func animateIn() { + guard let component = self.component else { + return + } + func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] { + let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) + + let x1 = sourcePoint.x + let y1 = sourcePoint.y + let x2 = midPoint.x + let y2 = midPoint.y + let x3 = targetPosition.x + let y3 = targetPosition.y + + var keyframes: [CGPoint] = [] + if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 { + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k + keyframes.append(CGPoint(x: x, y: y)) + } + } else { + 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)) + + 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(CGPoint(x: x, y: y)) + } + } + + return keyframes + } + + let playIconAnimation: (Double) -> Void = { duration in + self.avatarNode.contentNode.alpha = 0.0 + self.avatarNode.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + self.avatarNode.contentNode.layer.animateScale(from: 1.0, to: 0.01, duration: duration, removeOnCompletion: false) + + if let view = self.animation.view as? LottieComponent.View { + view.alpha = 1.0 + view.playOnce() + } + } + + if component.peer.id == component.context.account.peerId { + playIconAnimation(0.2) + } + + let offset = self.bounds.height - self.backgroundView.frame.minY + self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + if component.peer.id != component.context.account.peerId { + playIconAnimation(0.1) + } + HapticFeedback().success() + }) + + if let component = self.component { + let fromPoint = self.avatarNode.view.convert(component.sourceFrame.center, from: nil).offsetBy(dx: 0.0, dy: -offset) + let positionValues = generateAvatarParabollicMotionKeyframes(from: fromPoint, to: .zero, elevation: 20.0) + self.avatarNode.layer.animateKeyframes(values: positionValues.map { NSValue(cgPoint: $0) }, duration: 0.35, keyPath: "position", additive: true) + self.avatarNode.layer.animateScale(from: component.sourceFrame.width / self.avatarNode.bounds.width, to: 1.0, duration: 0.35) + } + + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.5)) + } + } + + func animateOut(completion: @escaping () -> Void) { + self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in + completion() + }) + self.backgroundView.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + + func update(component: QuickShareToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + if self.component == nil { + self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }) + } + + self.component = component + self.environment = environment + self.state = state + + let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0) + + let tabBarHeight: CGFloat + if !environment.safeInsets.left.isZero { + tabBarHeight = 34.0 + environment.safeInsets.bottom + } else { + tabBarHeight = 49.0 + environment.safeInsets.bottom + } + let containerInsets = UIEdgeInsets( + top: environment.safeInsets.top, + left: environment.safeInsets.left + 12.0, + bottom: tabBarHeight + 3.0, + right: environment.safeInsets.right + 12.0 + ) + + let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom) + + let spacing: CGFloat = 8.0 + + let iconSize = CGSize(width: 30.0, height: 30.0) + + let tooltipText: String + var overrideImage: AvatarNodeImageOverride? + var animationName: String = "anim_forward" + if component.peer.id == component.context.account.peerId { + tooltipText = environment.strings.Conversation_ForwardTooltip_SavedMessages_One + overrideImage = .savedMessagesIcon + animationName = "anim_savedmessages" + } else { + tooltipText = environment.strings.Conversation_ForwardTooltip_Chat_One(component.peer.compactDisplayTitle).string + } + + let contentSize = self.content.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .markdown( + text: tooltipText, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), + bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)), + link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), + linkAttribute: { _ in return nil }) + ))), + environment: {}, + containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - 16.0, height: availableContentSize.height) + ) + + var contentHeight: CGFloat = 0.0 + contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) + self.avatarNode.setPeer(context: component.context, theme: environment.theme, peer: component.peer, overrideImage: overrideImage, synchronousLoad: true) + self.avatarNode.updateSize(size: avatarFrame.size) + transition.setFrame(view: self.avatarNode.view, frame: avatarFrame) + + let _ = self.animation.update( + transition: transition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + size: CGSize(width: 38.0, height: 38.0), + loop: false + )), + environment: {}, + containerSize: iconSize + ) + if let animationView = self.animation.view { + if animationView.superview == nil { + animationView.alpha = 0.0 + self.avatarNode.view.addSubview(animationView) + } + animationView.frame = CGRect(origin: .zero, size: iconSize).insetBy(dx: -2.0, dy: -2.0) + } + + if let contentView = self.content.view { + if contentView.superview == nil { + self.contentView.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize)) + } + + let size = CGSize(width: availableContentSize.width, height: contentHeight) + let backgroundFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size) + + self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition) + + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size)) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class QuickShareToastScreen: ViewControllerComponentContainer { + private var processedDidAppear: Bool = false + private var processedDidDisappear: Bool = false + + init( + context: AccountContext, + peer: EnginePeer, + sourceFrame: CGRect, + action: @escaping () -> Void + ) { + super.init( + context: context, + component: QuickShareToastScreenComponent( + context: context, + peer: peer, + sourceFrame: sourceFrame, + action: action + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + presentationMode: .default, + updatedPresentationData: nil + ) + self.navigationPresentation = .flatModal + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.processedDidAppear { + self.processedDidAppear = true + if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View { + componentView.animateIn() + } + } + } + + private func superDismiss() { + super.dismiss() + } + + override func dismiss(completion: (() -> Void)? = nil) { + if !self.processedDidDisappear { + self.processedDidDisappear = true + + if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + if let self { + self.superDismiss() + } + completion?() + }) + } else { + super.dismiss(completion: completion) + } + } + } +} diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index b683b5687d..b42b850c86 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -280,7 +280,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let attemptedNavigationToPrivateQuote: (Peer?) -> Void public let forceUpdateWarpContents: () -> Void public let playShakeAnimation: () -> Void - public let displayQuickShare: (ASDisplayNode, ContextGesture) -> Void + public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -440,7 +440,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, forceUpdateWarpContents: @escaping () -> Void, playShakeAnimation: @escaping () -> Void, - displayQuickShare: @escaping (ASDisplayNode, ContextGesture) -> Void, + displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 732557021e..8e235da5ea 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -746,6 +746,7 @@ public final class ChatListHeaderComponent: Component { private var storyPeerList: ComponentView? public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)? + public var storyComposeAction: ((CGFloat) -> Void)? private var effectiveContentView: ContentView? { return self.secondaryContentView ?? self.primaryContentView @@ -977,6 +978,12 @@ public final class ChatListHeaderComponent: Component { return } self.component?.toggleIsLocked() + }, + composeAction: { [weak self] offset in + guard let self else { + return + } + self.storyComposeAction?(offset) } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 0d2fea8a25..a2edd0c5ed 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -130,8 +130,6 @@ final class GiftOptionsScreenComponent: Component { private let premiumTitle = ComponentView() private let premiumDescription = ComponentView() private var premiumItems: [AnyHashable: ComponentView] = [:] - private var inProgressPremiumGift: String? - private let purchaseDisposable = MetaDisposable() private let starsTitle = ComponentView() private let starsDescription = ComponentView() @@ -251,7 +249,6 @@ final class GiftOptionsScreenComponent: Component { deinit { self.starsStateDisposable?.dispose() - self.purchaseDisposable.dispose() } func scrollToTop() { @@ -262,21 +259,7 @@ final class GiftOptionsScreenComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) } - - private func dismissAllTooltips(controller: ViewController) { - controller.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - return true - }) - controller.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) - } - + private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { guard let environment = self.environment, let component = self.component else { return @@ -725,12 +708,8 @@ final class GiftOptionsScreenComponent: Component { let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 24.0 let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left - - let _ = bottomContentInset - let _ = sectionSpacing - + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled || state.disallowedGifts?.contains(.premium) == true @@ -1056,7 +1035,7 @@ final class GiftOptionsScreenComponent: Component { color: .purple ) }, - isLoading: self.inProgressPremiumGift == product.id + isLoading: false ) ), effectAlignment: .center, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 7bed9b2257..c48f21a471 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3744,7 +3744,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in @@ -10068,7 +10068,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro cameraTransitionIn = StoryCameraTransitionIn( sourceView: rightButton.view, sourceRect: rightButton.view.bounds, - sourceCornerRadius: rightButton.view.bounds.height * 0.5 + sourceCornerRadius: rightButton.view.bounds.height * 0.5, + useFillAnimation: false ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index 6658f5726d..faa2fbb5e0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -1,4 +1,44 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "StoryPeerListMetalResources", + srcs = glob([ + "MetalResources/**/*.*", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "StoryPeerListBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.StoryPeerList + CFBundleDevelopmentRegion + en + CFBundleName + StoryPeerList + """ +) + +apple_resource_bundle( + name = "StoryPeerListBundle", + infoplists = [ + ":StoryPeerListBundleInfoPlist", + ], + resources = [ + ":StoryPeerListMetalResources", + ], +) swift_library( name = "StoryPeerListComponent", @@ -8,9 +48,13 @@ swift_library( ]), copts = [ "-warnings-as-errors", + ], + data = [ + ":StoryPeerListBundle", ], deps = [ "//submodules/Display", + "//submodules/MetalEngine", "//submodules/ComponentFlow", "//submodules/AppBundle", "//submodules/Components/BundleIconComponent", diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal new file mode 100644 index 0000000000..5f1848a3d7 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal @@ -0,0 +1,109 @@ +#include +using namespace metal; + +struct QuadVertexOut { + float4 position [[position]]; + float2 uv; +}; + +constant static float2 quadVertices[6] = { + float2(0.0, 0.0), + float2(1.0, 0.0), + float2(0.0, 1.0), + float2(1.0, 0.0), + float2(0.0, 1.0), + float2(1.0, 1.0) +}; + +vertex QuadVertexOut cameraBlobVertex( + constant float4 &rect [[ buffer(0) ]], + uint vid [[ vertex_id ]] +) { + float2 quadVertex = quadVertices[vid]; + + QuadVertexOut out; + out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0); + out.position.x = -1.0 + out.position.x * 2.0; + out.position.y = -1.0 + out.position.y * 2.0; + + out.uv = quadVertex; + + return out; +} + +#define BindingDistance 0.45 +#define AARadius 2.0 +#define PersistenceFactor 1.1 + +float smin(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0); + h = pow(h, 0.6); + return mix(a, b, h) - k * h * (1.0 - h) * 1.0; +} + +float sdfCircle(float2 uv, float2 position, float radius) { + return length(uv - position) - radius; +} + +float dynamicBinding(float2 primaryPos, float2 secondaryPos) { + float distance = length(primaryPos - secondaryPos); + return BindingDistance * (1.0 + PersistenceFactor * smoothstep(0.0, 2.0, distance)); +} + +float map(float2 uv, float2 primaryParameters, float2 primaryOffset, float2 secondaryParameters, float2 secondaryOffset) { + float primary = sdfCircle(uv, primaryOffset, primaryParameters.x); + float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x); + + float bindDist = dynamicBinding(primaryOffset, secondaryOffset); + + float metaballs = 1.0; + metaballs = smin(metaballs, primary, bindDist); + metaballs = smin(metaballs, secondary, bindDist); + return metaballs; +} + +fragment half4 cameraBlobFragment( + QuadVertexOut in [[stage_in]], + constant float2 &primaryParameters [[buffer(0)]], + constant float2 &primaryOffset [[buffer(1)]], + constant float2 &secondaryParameters [[buffer(2)]], + constant float2 &secondaryOffset [[buffer(3)]], + constant float2 &resolution [[buffer(4)]] +) { + float aspectRatio = resolution.x / resolution.y; + float2 uv = in.uv * 2.0 - 1.0; + uv.x *= aspectRatio; + + float t = AARadius / resolution.y; + + float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset)); + + if (primaryParameters.y > 0) { + float innerHoleRadius = primaryParameters.y; + float hole = smoothstep(-t, t, length(uv - primaryOffset) - innerHoleRadius); + float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2)); + + c *= mix(1.0, hole, primaryInfluence); + } else if (primaryParameters.y < 0) { + float cutRadius = abs(primaryParameters.y); + float2 primaryFeatheredOffset = primaryOffset; + primaryFeatheredOffset.x *= 1.129; + + float distFromCenter = length(uv - primaryFeatheredOffset); + + float gradientWidth = 0.21; + float featheredEdge = smoothstep(cutRadius - gradientWidth, cutRadius + gradientWidth, distFromCenter); + + float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2)); + + float horizontalFade = 1.0; + float rightEdgePosition = 0.94 * aspectRatio; + if (uv.x > rightEdgePosition) { + horizontalFade = 1.0 - smoothstep(0.0, 0.22, uv.x - rightEdgePosition); + } + + c *= mix(1.0, featheredEdge, primaryInfluence) * horizontalFade; + } + + return half4(0.0, 0.0, 0.0, c); +} diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift new file mode 100644 index 0000000000..1ebf14db78 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift @@ -0,0 +1,545 @@ +import Foundation +import Display +import Metal +import MetalKit +import MetalEngine +import ComponentFlow +import TelegramPresentationData + + +private var metalLibraryValue: MTLLibrary? +func metalLibrary(device: MTLDevice) -> MTLLibrary? { + if let metalLibraryValue { + return metalLibraryValue + } + + let mainBundle = Bundle(for: StoryBlobLayer.self) + guard let path = mainBundle.path(forResource: "StoryPeerListBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { + return nil + } + + metalLibraryValue = library + return library +} + +private final class PropertyAnimation { + let from: T + let to: T + let animation: ComponentTransition.Animation + let startTimestamp: Double + private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable + + init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) { + self.from = fromValue + self.to = toValue + self.animation = animation + self.startTimestamp = startTimestamp + self.interpolator = T.interpolator() + } + + func valueAt(_ t: CGFloat) -> Interpolatable { + if t <= 0.0 { + return self.from + } else if t >= 1.0 { + return self.to + } else { + return self.interpolator(self.from, self.to, t) + } + } +} + +private final class AnimatableProperty { + var presentationValue: T + var value: T + private var animation: PropertyAnimation? + + init(value: T) { + self.value = value + self.presentationValue = value + } + + func update(value: T, transition: ComponentTransition = .immediate) { + let currentTimestamp = CACurrentMediaTime() + if case .none = transition.animation { + if let animation = self.animation, case let .curve(duration, curve) = animation.animation { + self.value = value + let elapsed = duration - (currentTimestamp - animation.startTimestamp) + if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) + } else { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) + } + } else { + self.value = value + self.presentationValue = value + self.animation = nil + } + } else { + self.value = value + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) + } + } + + func tick(timestamp: Double) -> Bool { + guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else { + return false + } + + let timeFromStart = timestamp - animation.startTimestamp + var t = max(0.0, timeFromStart / duration) + switch curve { + case .linear: + break + case .easeInOut: + t = listViewAnimationCurveEaseInOut(t) + case .spring: + t = lookupSpringValue(t) + case let .custom(x1, y1, x2, y2): + t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) + } + self.presentationValue = animation.valueAt(t) as! T + + if timeFromStart <= duration { + return true + } + self.animation = nil + return false + } +} + +private func lookupSpringValue(_ t: CGFloat) -> CGFloat { + let table: [(CGFloat, CGFloat)] = [ + (0.0, 0.0), + (0.0625, 0.1123005598783493), + (0.125, 0.31598418951034546), + (0.1875, 0.5103585720062256), + (0.25, 0.6650152802467346), + (0.3125, 0.777747631072998), + (0.375, 0.8557760119438171), + (0.4375, 0.9079672694206238), + (0.5, 0.942038357257843), + (0.5625, 0.9638798832893372), + (0.625, 0.9776856303215027), + (0.6875, 0.9863143563270569), + (0.75, 0.991658091545105), + (0.8125, 0.9949421286582947), + (0.875, 0.9969474077224731), + (0.9375, 0.9981651306152344), + (1.0, 1.0) + ] + + for i in 0 ..< table.count - 2 { + let lhs = table[i] + let rhs = table[i + 1] + + if t >= lhs.0 && t <= rhs.0 { + let fraction = (t - lhs.0) / (rhs.0 - lhs.0) + let value = lhs.1 + fraction * (rhs.1 - lhs.1) + return value + } + } + return 1.0 +} + +final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject { + var internalData: MetalEngineSubjectInternalData? + + private final class RenderState: RenderToLayerState { + let pipelineState: MTLRenderPipelineState + + required init?(device: MTLDevice) { + guard let library = metalLibrary(device: device) else { + return nil + } + guard let vertexFunction = library.makeFunction(name: "cameraBlobVertex"), + let fragmentFunction = library.makeFunction(name: "cameraBlobFragment") else { + return nil + } + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha + + guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { + return nil + } + self.pipelineState = pipelineState + } + } + + private var primarySize = AnimatableProperty(value: 1.0) + private var primaryHoleSize = AnimatableProperty(value: 0.0) + private var primaryOffsetX = AnimatableProperty(value: 45.0) + private var primaryOffsetY = AnimatableProperty(value: 0.0) + + private var secondarySize = AnimatableProperty(value: 0.85) + private var secondaryOffsetX = AnimatableProperty(value: 45.0) + private var secondaryOffsetY = AnimatableProperty(value: 0.0) + + private var displayLinkSubscription: SharedDisplayLinkDriver.Link? + private var hasActiveAnimations: Bool = false + + override init() { + super.init() + + self.isOpaque = false + + self.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in + guard let self else { + return + } + self.updateAnimations() + if self.hasActiveAnimations { + self.setNeedsUpdate() + } + } + } + + self.didExitHierarchy = { [weak self] in + guard let self else { + return + } + self.displayLinkSubscription = nil + } + } + + override init(layer: Any) { + super.init(layer: layer) + + if let layer = layer as? StoryBlobLayer { + self.primarySize = layer.primarySize + self.primaryHoleSize = layer.primaryHoleSize + self.primaryOffsetX = layer.primaryOffsetX + self.primaryOffsetY = layer.primaryOffsetY + self.secondarySize = layer.secondarySize + self.secondaryOffsetX = layer.secondaryOffsetX + self.secondaryOffsetY = layer.secondaryOffsetY + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePrimarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.height > 0.0 else { + return + } + let mappedSize = size / self.bounds.height + self.primarySize.update(value: mappedSize, transition: transition) + + self.setNeedsUpdate() + } + + func updatePrimaryHoleSize(_ size: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.height > 0.0 else { + return + } + let mappedSize = size / self.bounds.height + self.primaryHoleSize.update(value: mappedSize, transition: transition) + + self.setNeedsUpdate() + } + + func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.height > 0.0 else { + return + } + let mappedOffset = offset / self.bounds.height * 2.0 + self.primaryOffsetX.update(value: mappedOffset, transition: transition) + + self.setNeedsUpdate() + } + + func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.width > 0.0 else { + return + } + let mappedOffset = offset / self.bounds.width * 2.0 + self.primaryOffsetY.update(value: mappedOffset, transition: transition) + + self.setNeedsUpdate() + } + + func updateSecondarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.height > 0.0 else { + return + } + let mappedSize = size / self.bounds.height + self.secondarySize.update(value: mappedSize, transition: transition) + + self.setNeedsUpdate() + } + + func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.height > 0.0 else { + return + } + let mappedOffset = offset / self.bounds.height * 2.0 + self.secondaryOffsetX.update(value: mappedOffset, transition: transition) + + self.setNeedsUpdate() + } + + func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) { + guard self.bounds.width > 0.0 else { + return + } + let mappedOffset = offset / self.bounds.width * 2.0 + self.secondaryOffsetY.update(value: mappedOffset, transition: transition) + + self.setNeedsUpdate() + } + + private func updateAnimations() { + let properties = [ + self.primarySize, + self.primaryHoleSize, + self.primaryOffsetX, + self.primaryOffsetY, + self.secondarySize, + self.secondaryOffsetX, + self.secondaryOffsetY, + ] + + let timestamp = CACurrentMediaTime() + var hasAnimations = false + for property in properties { + if property.tick(timestamp: timestamp) { + hasAnimations = true + } + } + self.hasActiveAnimations = hasAnimations + } + + func update(context: MetalEngineSubjectContext) { + if self.bounds.isEmpty { + return + } + + let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale) + + context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, commands: { encoder, placement in + let effectiveRect = placement.effectiveRect + + var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height)) + encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) + + var primaryParameters = simd_float2( + Float(self.primarySize.presentationValue), + Float(self.primaryHoleSize.presentationValue) + ) + encoder.setFragmentBytes(&primaryParameters, length: MemoryLayout.size, index: 0) + + var primaryOffset = simd_float2( + Float(self.primaryOffsetX.presentationValue), + Float(self.primaryOffsetY.presentationValue) + ) + encoder.setFragmentBytes(&primaryOffset, length: MemoryLayout.size, index: 1) + + var secondaryParameters = simd_float2( + Float(self.secondarySize.presentationValue), + Float(0) + ) + encoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout.size, index: 2) + + var secondaryOffset = simd_float2( + Float(self.secondaryOffsetX.presentationValue), + Float(self.secondaryOffsetY.presentationValue) + ) + encoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout.size, index: 3) + + var resolution = simd_float2( + Float(drawableSize.width), + Float(drawableSize.height) + ) + encoder.setFragmentBytes(&resolution, length: MemoryLayout.size, index: 4) + + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) + }) + } +} + +final class StoryComposeLayer: CALayer { + private let theme: PresentationTheme? + private let strings: PresentationStrings? + + private let blobLayer = SimpleLayer() + private let maskLayer = StoryBlobLayer() + private let backgroundLayer = SimpleGradientLayer() + private let foregroundLayer = SimpleGradientLayer() + private let iconLayer = SimpleLayer() + private let labelLayer = SimpleLayer() + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + super.init() + + self.addSublayer(self.blobLayer) + + self.blobLayer.mask = self.maskLayer + self.blobLayer.masksToBounds = true + + self.backgroundLayer.type = .axial + self.backgroundLayer.startPoint = CGPoint(x: 0.5, y: 0.0) + self.backgroundLayer.endPoint = CGPoint(x: 0.5, y: 1.0) + self.blobLayer.addSublayer(self.backgroundLayer) + + self.foregroundLayer.type = .axial + self.foregroundLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.foregroundLayer.endPoint = CGPoint(x: 1.0, y: 0.5) + self.blobLayer.addSublayer(self.foregroundLayer) + + self.iconLayer.contents = generateAddIcon(color: theme.list.itemCheckColors.foregroundColor)?.cgImage + self.iconLayer.opacity = 0.0 + self.blobLayer.addSublayer(self.iconLayer) + + if let image = generateAddLabel(strings: strings, color: theme.list.itemPrimaryTextColor) { + self.labelLayer.contents = image.cgImage + self.labelLayer.bounds = CGRect(origin: .zero, size: image.size) + self.addSublayer(self.labelLayer) + self.labelLayer.opacity = 0.0 + } + + if let blurEffect = CALayer.blur() { + self.iconLayer.filters = [blurEffect] + blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius") + self.labelLayer.filters = [blurEffect] + blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius") + } + } + + override init(layer: Any) { + if let layer = layer as? StoryComposeLayer { + self.theme = layer.theme + self.strings = layer.strings + } else { + self.theme = nil + self.strings = nil + } + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateOffset(_ offset: CGFloat, baseSize: CGFloat, colors: [CGColor], transition: ComponentTransition = .immediate) { + self.backgroundLayer.colors = colors + self.foregroundLayer.locations = [0.0, 0.68, 1.0] + + if let theme = self.theme { + let fillColor = theme.list.itemCheckColors.fillColor + self.foregroundLayer.colors = [ + fillColor.cgColor, + fillColor.withAlphaComponent(0.0).cgColor, + fillColor.withAlphaComponent(0.0).cgColor + ] + } + + var holeSize = baseSize > 52.0 ? 58.0 : -60.0 + var primaryBaseScale = 1.0 + var secondaryBaseScale = 0.68 + if holeSize < 0.0 { + if offset > 48.0 { + holeSize -= min(14.0, (max(0.0, (offset - 48.0)) / 20.0) * 14.0) + } + primaryBaseScale = 0.96 + min(1.0, max(0.0, (offset - 20.0)) / 10.0) * 0.04 + secondaryBaseScale = 0.62 + } + + var secondaryScale = secondaryBaseScale + if offset < 20.0 { + secondaryScale += (offset / 20.0) * 0.2 + } else { + secondaryScale = min(1.0, max(secondaryBaseScale + 0.2, offset / 75.0)) + } + + self.maskLayer.updatePrimaryOffsetX(85.0, transition: transition) + self.maskLayer.updatePrimarySize(baseSize * primaryBaseScale, transition: transition) + self.maskLayer.updateSecondaryOffsetX(85.0 - offset, transition: transition) + self.maskLayer.updatePrimaryHoleSize(holeSize, transition: transition) + self.maskLayer.updateSecondarySize(55.0 * secondaryScale, transition: transition) + + if holeSize < 0.0 { + transition.setAlpha(layer: self.blobLayer, alpha: min(1.0, max(0.0, offset / 10.0))) + } + + let layerX = self.bounds.width - offset + 5.0 + transition.setPosition(layer: self.iconLayer, position: CGPoint(x: layerX, y: self.bounds.height / 2.0)) + transition.setPosition(layer: self.labelLayer, position: CGPoint(x: layerX, y: self.bounds.height + 13.0 - UIScreenPixel)) + + let iconOffset = max(0.0, offset - 35.0) + let alpha = max(0.0, min(1.0, iconOffset / 15.0)) + let blurRadius = 8.0 - min(8.0, (iconOffset / 20.0) * 8.0) + + for layer in [self.iconLayer, self.labelLayer] { + layer.setValue(blurRadius as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius") + transition.setAlpha(layer: layer, alpha: alpha) + } + } + + override func layoutSublayers() { + super.layoutSublayers() + + self.blobLayer.frame = self.bounds + self.maskLayer.frame = self.bounds + self.backgroundLayer.frame = self.bounds + self.foregroundLayer.frame = self.bounds + self.iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)) + } +} + +private func generateAddIcon(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(3.0) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 15.0, y: 5.5)) + context.addLine(to: CGPoint(x: 15.0, y: 24.5)) + context.strokePath() + + context.move(to: CGPoint(x: 5.5, y: 15.0)) + context.addLine(to: CGPoint(x: 24.5, y: 15.0)) + context.strokePath() + }) +} + + +private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? { + let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center) + var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil) + textRect.size.width = ceil(textRect.size.width) + textRect.size.height = ceil(textRect.size.height) + + return generateImage(textRect.size, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + UIGraphicsPushContext(context) + titleString.draw(in: textRect) + UIGraphicsPopContext() + }) +} diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index c28dabc273..502f8a5f53 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -60,6 +60,7 @@ public final class StoryPeerListComponent: Component { public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void public let openStatusSetup: (UIView) -> Void public let lockAction: () -> Void + public let composeAction: (CGFloat) -> Void public init( externalState: ExternalState, @@ -81,7 +82,8 @@ public final class StoryPeerListComponent: Component { peerAction: @escaping (EnginePeer?) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, openStatusSetup: @escaping (UIView) -> Void, - lockAction: @escaping () -> Void + lockAction: @escaping () -> Void, + composeAction: @escaping (CGFloat) -> Void ) { self.externalState = externalState self.context = context @@ -103,6 +105,7 @@ public final class StoryPeerListComponent: Component { self.contextPeerAction = contextPeerAction self.openStatusSetup = openStatusSetup self.lockAction = lockAction + self.composeAction = composeAction } public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { @@ -162,7 +165,6 @@ public final class StoryPeerListComponent: Component { private final class VisibleItem { let view = ComponentView() - var hasBlur: Bool = false init() { } @@ -360,6 +362,8 @@ public final class StoryPeerListComponent: Component { private var sharedBlurEffect: NSObject? + private var willComposeOnRelease = false + public override init(frame: CGRect) { self.collapsedButton = HighlightableButton() @@ -561,14 +565,62 @@ public final class StoryPeerListComponent: Component { public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) + + let willComposeOnRelease = scrollView.contentOffset.x <= -70.0 + if self.willComposeOnRelease != willComposeOnRelease { + self.willComposeOnRelease = willComposeOnRelease + + if willComposeOnRelease { + HapticFeedback().tap() + } else { + HapticFeedback().impact(.veryLight) + } + } + + if scrollView.isScrollEnabled && scrollView.isTracking, scrollView.contentOffset.x <= -85.0 { + scrollView.isScrollEnabled = false + scrollView.panGestureRecognizer.isEnabled = false + scrollView.panGestureRecognizer.isEnabled = true + scrollView.contentOffset = CGPoint(x: -85.0, y: 0.0) + + self.willComposeOnRelease = false + Queue.mainQueue().after(0.5) { + scrollView.isScrollEnabled = true + scrollView.contentOffset = .zero + } + if let component = self.component { + HapticFeedback().tap() + component.composeAction(abs(scrollView.contentOffset.x)) + } + } } } + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !self.ignoreScrolling { + if scrollView.isScrollEnabled && scrollView.contentOffset.x <= -70.0 { + scrollView.isScrollEnabled = false + scrollView.panGestureRecognizer.isEnabled = false + scrollView.panGestureRecognizer.isEnabled = true + scrollView.contentOffset = CGPoint(x: max(-85.0, scrollView.contentOffset.x), y: 0.0) + + self.willComposeOnRelease = false + Queue.mainQueue().after(0.5) { + scrollView.isScrollEnabled = true + scrollView.contentOffset = .zero + } + if let component = self.component { + HapticFeedback().tap() + component.composeAction(abs(scrollView.contentOffset.x)) + } + } + } + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } - let titleIconSpacing: CGFloat = 4.0 let titleIndicatorSpacing: CGFloat = 8.0 @@ -1072,6 +1124,11 @@ public final class StoryPeerListComponent: Component { totalCount = itemSet.storyCount unseenCount = itemSet.unseenCount + var composeContentOffset: CGFloat? + if peer.id == component.context.account.peerId && collapsedState.sideAlphaFraction == 1.0 && self.scrollView.contentOffset.x < 0.0 { + composeContentOffset = self.scrollView.contentOffset.x * -1.0 + } + let _ = visibleItem.view.update( transition: itemTransition, component: AnyComponent(StoryPeerListItemComponent( @@ -1090,6 +1147,7 @@ public final class StoryPeerListComponent: Component { expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, + composeContentOffset: composeContentOffset, action: component.peerAction, contextGesture: component.contextPeerAction )), @@ -1228,6 +1286,7 @@ public final class StoryPeerListComponent: Component { expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, + composeContentOffset: nil, action: component.peerAction, contextGesture: component.contextPeerAction )), diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index b4a488ae51..eab96745c9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -380,6 +380,7 @@ public final class StoryPeerListItemComponent: Component { public let expandEffectFraction: CGFloat public let leftNeighborDistance: CGPoint? public let rightNeighborDistance: CGPoint? + public let composeContentOffset: CGFloat? public let action: (EnginePeer) -> Void public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void @@ -399,6 +400,7 @@ public final class StoryPeerListItemComponent: Component { expandEffectFraction: CGFloat, leftNeighborDistance: CGPoint?, rightNeighborDistance: CGPoint?, + composeContentOffset: CGFloat?, action: @escaping (EnginePeer) -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void ) { @@ -417,6 +419,7 @@ public final class StoryPeerListItemComponent: Component { self.expandEffectFraction = expandEffectFraction self.leftNeighborDistance = leftNeighborDistance self.rightNeighborDistance = rightNeighborDistance + self.composeContentOffset = composeContentOffset self.action = action self.contextGesture = contextGesture } @@ -467,6 +470,9 @@ public final class StoryPeerListItemComponent: Component { if lhs.rightNeighborDistance != rhs.rightNeighborDistance { return false } + if lhs.composeContentOffset != rhs.composeContentOffset { + return false + } return true } @@ -479,6 +485,7 @@ public final class StoryPeerListItemComponent: Component { private let button: HighlightTrackingButton + fileprivate var composeLayer: StoryComposeLayer? fileprivate let avatarContent: PortalSourceView private let avatarContainer: UIView private let avatarBackgroundContainer: UIView @@ -494,12 +501,11 @@ public final class StoryPeerListItemComponent: Component { private let indicatorShapeSeenLayer: SimpleShapeLayer private let indicatorShapeUnseenLayer: SimpleShapeLayer private let title = ComponentView() + private let composeTitle = ComponentView() private var component: StoryPeerListItemComponent? private weak var componentState: EmptyComponentState? - private var demoLoading = false - public override init(frame: CGRect) { self.backgroundContainer = UIView() self.backgroundContainer.isUserInteractionEnabled = false @@ -960,6 +966,32 @@ public final class StoryPeerListItemComponent: Component { self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height)) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + var baseSize: CGFloat = 60.0 + var effectiveColors = component.unseenCount > 0 ? unseenColors : seenColors + if self.avatarAddBadgeView != nil { + baseSize = 52.0 + effectiveColors = [component.theme.list.itemCheckColors.fillColor.cgColor, component.theme.list.itemCheckColors.fillColor.cgColor] + } + if let composeContentOffset = component.composeContentOffset { + let composeLayer: StoryComposeLayer + if let current = self.composeLayer { + composeLayer = current + } else { + composeLayer = StoryComposeLayer(theme: component.theme, strings: component.strings) + self.composeLayer = composeLayer + self.layer.addSublayer(composeLayer) + } + + composeLayer.frame = CGRect(origin: CGPoint(x: size.width - 195.0, y: 0.0), size: CGSize(width: 160.0, height: 60.0)) + composeLayer.updateOffset(composeContentOffset, baseSize: baseSize, colors: effectiveColors, transition: transition) + } else if let composeLayer = self.composeLayer { + self.composeLayer = nil + composeLayer.updateOffset(0.0, baseSize: baseSize, colors: effectiveColors, transition: .easeInOut(duration: 0.2)) + Queue.mainQueue().after(0.21, { + composeLayer.removeFromSuperlayer() + }) + } + return availableSize } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index deddc79e90..1ee57f9110 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -418,7 +418,7 @@ extension ChatControllerImpl { } } - strongSelf.dismissPreviewing?() + let _ = strongSelf.dismissPreviewing?(false) } })) case .replyThread: diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index f6fd011cfa..e0d05c58b9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -292,11 +292,20 @@ extension ChatControllerImpl { } } + var keepDefaultContentTouches = false + for media in message.media { + if media is TelegramMediaImage { + keepDefaultContentTouches = true + } else if let file = media as? TelegramMediaFile, file.isVideo { + keepDefaultContentTouches = true + } + } + let source: ContextContentSource if let location = location { source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) } else { - source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll)) + source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll, keepDefaultContentTouches: keepDefaultContentTouches)) } self.canReadHistory.set(false) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerQuickShare.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerQuickShare.swift index eed527a06d..202366bcb3 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerQuickShare.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerQuickShare.swift @@ -1,15 +1,52 @@ import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit +import TelegramCore import ContextUI import QuickShareScreen extension ChatControllerImpl { - func displayQuickShare(node: ASDisplayNode, gesture: ContextGesture) { - guard !"".isEmpty else { - return - } - let controller = QuickShareScreen(context: self.context, sourceNode: node, gesture: gesture) + func displayQuickShare(id: EngineMessage.Id, node: ASDisplayNode, gesture: ContextGesture) { + let controller = QuickShareScreen( + context: self.context, + sourceNode: node, + gesture: gesture, + openPeer: { [weak self] peerId in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) + }) + }, + completion: { [weak self] peerId in + guard let self else { + return + } + let enqueueMessage = StandaloneSendEnqueueMessage( + content: .forward(forward: StandaloneSendEnqueueMessage.Forward( + sourceId: id, + threadId: nil + )), + replyToMessageId: nil + ) + let _ = (standaloneSendEnqueueMessages( + accountPeerId: self.context.account.peerId, + postbox: self.context.account.postbox, + network: self.context.account.network, + stateManager: self.context.account.stateManager, + auxiliaryMethods: self.context.account.auxiliaryMethods, + peerId: peerId, + threadId: nil, + messages: [enqueueMessage] + )).startStandalone() + } + ) self.presentInGlobalOverlay(controller) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ffde3d3294..c989df76e6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -250,7 +250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var botStart: ChatControllerInitialBotStart? var attachBotStart: ChatControllerInitialAttachBotStart? var botAppStart: ChatControllerInitialBotAppStart? - let mode: ChatControllerPresentationMode + var mode: ChatControllerPresentationMode let peerDisposable = MetaDisposable() let titleDisposable = MetaDisposable() @@ -575,7 +575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)? public var purposefulAction: (() -> Void)? - public var dismissPreviewing: (() -> Void)? + public var dismissPreviewing: ((Bool) -> (() -> Void))? var updatedClosedPinnedMessageId: ((MessageId) -> Void)? var requestedUnpinAllMessages: ((Int, MessageId) -> Void)? @@ -893,6 +893,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } + if let contextController = self.currentContextController { + self.present(contextController, in: .window(.root)) + Queue.mainQueue().after(0.15) { + contextController.dismiss(result: .dismissWithoutContent, completion: nil) + } + } + let mode = params.mode let displayVoiceMessageDiscardAlert: () -> Bool = { [weak self] in @@ -1360,7 +1367,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view) + if let contextController = self.currentContextController { + contextController.view.addSubview(view) + } else { + self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view) + } }, openUrl: { [weak self] url in self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil) }, openPeer: { [weak self] peer, navigation in @@ -4850,11 +4861,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.playShakeAnimation() - }, displayQuickShare: { [weak self] node, gesture in + }, displayQuickShare: { [weak self] messageId, node, gesture in guard let self else { return } - self.displayQuickShare(node: node, gesture: gesture) + self.displayQuickShare(id: messageId, node: node, gesture: gesture) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -7458,11 +7469,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { + self.mode = mode self.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedMode(mode) }) } + func animateFromPreviewing(transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)) { + guard let navigationController = self.effectiveNavigationController else { + return + } + self.mode = .standard(.default) + let completion = self.dismissPreviewing?(true) + + let initialLayout = self.validLayout + let initialFrame = self.view.convert(self.view.bounds, to: navigationController.view) + + navigationController.pushViewController(self, animated: false) + + let updatedLayout = self.validLayout + let updatedFrame = self.view.frame + + if let initialLayout, let updatedLayout, transition.isAnimated { + let initialView = self.view.superview + navigationController.view.addSubview(self.view) + + self.view.clipsToBounds = true + self.view.frame = initialFrame + self.containerLayoutUpdated(initialLayout, transition: .immediate) + self.containerLayoutUpdated(updatedLayout, transition: transition) + + self.view.layer.animate(from: 14.0, to: updatedLayout.deviceMetrics.screenCornerRadius, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4) + + transition.updateFrame(view: self.view, frame: updatedFrame, completion: { _ in + initialView?.addSubview(self.view) + self.view.clipsToBounds = false + + completion?() + }) + transition.updateCornerRadius(layer: self.view.layer, cornerRadius: 0.0) + } + + if let navigationBar = self.navigationBar { + let nodes = [ + navigationBar.backButtonNode, + navigationBar.backButtonArrow, + navigationBar.badgeNode + ] + for node in nodes { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + + self.canReadHistory.set(true) + + self.updateChatPresentationInterfaceState(transition: transition, interactive: false) { state in + return state.updatedMode(self.mode) + } + } + var chatDisplayNode: ChatControllerNode { get { return super.displayNode as! ChatControllerNode diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index f6aeb45830..d9f6f31874 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -3453,7 +3453,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if recognizer.state == .ended { - self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view)) + if case .standard(.previewing) = self.chatPresentationInterfaceState.mode { + self.controller?.animateFromPreviewing() + } else { + self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view)) + } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 0bce425e9b..b35c4714af 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -33,6 +33,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou let ignoreContentTouches: Bool = false let blurBackground: Bool = true let centerVertically: Bool + let keepDefaultContentTouches: Bool private weak var chatController: ChatControllerImpl? private weak var chatNode: ChatControllerNode? @@ -59,13 +60,14 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou |> distinctUntilChanged } - init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) { + init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false, keepDefaultContentTouches: Bool = false) { self.chatController = chatController self.chatNode = chatNode self.engine = engine self.message = message self.selectAll = selectAll self.centerVertically = centerVertically + self.keepDefaultContentTouches = keepDefaultContentTouches } func takeView() -> ContextControllerTakeViewInfo? { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 39d71dd3e0..7b485f6ed6 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -192,7 +192,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e927a98996..31962b885a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2185,7 +2185,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { - }, displayQuickShare: { _ ,_ in + }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index ea42113a95..78099bd0fa 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -327,7 +327,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return CameraScreenImpl.TransitionIn( sourceView: sourceView, sourceRect: $0.sourceRect, - sourceCornerRadius: $0.sourceCornerRadius + sourceCornerRadius: $0.sourceCornerRadius, + useFillAnimation: $0.useFillAnimation ) } else { return nil @@ -527,8 +528,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return StoryCameraTransitionInCoordinator( animateIn: { [weak cameraController] in if let cameraController { - cameraController.updateTransitionProgress(0.0, transition: .immediate) - cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false) + if transitionIn?.useFillAnimation == true { + cameraController.animateIn() + } else { + cameraController.updateTransitionProgress(0.0, transition: .immediate) + cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false) + } } }, updateTransitionProgress: { [weak cameraController] transitionFraction in