diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index eaa069688d..e3baf38849 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -156,7 +156,10 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { let point = recognizer.location(in: self.view) let currentHitView = self.hitTest(point, with: nil) - let scrollViewAndListNode = self.findScrollView(view: currentHitView) + var scrollViewAndListNode = self.findScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } let scrollView = scrollViewAndListNode?.0 let listNode = scrollViewAndListNode?.1 diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index e8fd0a8103..5a11fbd3b4 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -90,6 +90,7 @@ private func generateMaskImage() -> UIImage? { public class AttachmentController: ViewController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? + private let chatLocation: ChatLocation private let buttons: [AttachmentButtonType] public var mediaPickerContext: AttachmentMediaPickerContext? { @@ -164,7 +165,7 @@ public class AttachmentController: ViewController { self.container = AttachmentContainer() self.container.canHaveKeyboardFocus = true - self.panel = AttachmentPanel(context: controller.context, updatedPresentationData: controller.updatedPresentationData) + self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData) super.init() @@ -360,9 +361,9 @@ public class AttachmentController: ViewController { if case .compact = layout.metrics.widthClass { let offset = strongSelf.container.isExpanded ? 10.0 : 24.0 - strongSelf.container.clipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.18, removeOnCompletion: false, additive: true, completion: { [weak self] _ in + strongSelf.container.clipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.18, removeOnCompletion: false, additive: true, completion: { [weak self] _ in if let strongSelf = self { - strongSelf.container.clipNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)), to: NSValue(cgPoint: CGPoint(x: 0.0, y: offset)), keyPath: "position", duration: 0.55, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in + strongSelf.container.clipNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)), to: NSValue(cgPoint: CGPoint(x: 0.0, y: -offset)), keyPath: "position", duration: 0.55, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in self?.container.clipNode.layer.removeAllAnimations() self?.animating = false }) @@ -544,10 +545,11 @@ public class AttachmentController: ViewController { completion(nil, nil) } - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, buttons: [AttachmentButtonType]) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType]) { self.context = context - self.buttons = buttons self.updatedPresentationData = updatedPresentationData + self.chatLocation = chatLocation + self.buttons = buttons super.init(navigationBarPresentationData: nil) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 18bb0b80b5..28937571d7 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -182,11 +182,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { var present: (ViewController) -> Void = { _ in } var presentInGlobalOverlay: (ViewController) -> Void = { _ in } - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?) { + init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal)?) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) self.containerNode = ASDisplayNode() self.containerNode.clipsToBounds = true diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryController.h index 7f6c989d28..533f54948b 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryController.h @@ -40,6 +40,8 @@ typedef NS_ENUM(NSUInteger, TGModernGalleryScrollAnimationDirection) { - (TGModernGalleryItemView *)itemViewForItem:(id)item; - (id)currentItem; +- (UIView *)transitionView; + - (void)setCurrentItemIndex:(NSUInteger)index animated:(bool)animated; - (void)setCurrentItemIndex:(NSUInteger)index direction:(TGModernGalleryScrollAnimationDirection)direction animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryItemView.h index 33ca21cc4b..5746499959 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryItemView.h @@ -54,6 +54,7 @@ - (UIView *)footerView; - (UIView *)transitionView; +- (UIView *)transitionContentView; - (CGRect)transitionViewContentRect; - (bool)dismissControllerNowOrSchedule; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryZoomableItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryZoomableItemView.h index c3868f792f..a8c2d2e64d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryZoomableItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernGalleryZoomableItemView.h @@ -9,7 +9,6 @@ - (CGSize)contentSize; - (UIView *)contentView; -- (UIView *)transitionContentView; - (void)reset; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 9378e9fd35..9575cd2853 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -876,6 +876,14 @@ return [_imageView convertRect:_imageView.bounds toView:[self transitionView]]; } +- (UIView *)transitionContentView { + if (_videoView != nil) { + return _videoView; + } else { + return _imageView; + } +} + - (UIImage *)screenImage { if (_videoView != nil) diff --git a/submodules/LegacyComponents/Sources/TGModernGalleryController.m b/submodules/LegacyComponents/Sources/TGModernGalleryController.m index d2bd0a5a07..e594529dd5 100644 --- a/submodules/LegacyComponents/Sources/TGModernGalleryController.m +++ b/submodules/LegacyComponents/Sources/TGModernGalleryController.m @@ -178,6 +178,27 @@ } } +- (UIView *)transitionView { + id focusItem = nil; + if ([self currentItemIndex] < self.model.items.count) + focusItem = self.model.items[[self currentItemIndex]]; + + for (TGModernGalleryItemView *itemView in self->_visibleItemViews) + { + if ([itemView.item isEqual:focusItem]) + { + itemView.alpha = 0.01; + UIView *contentView = [itemView transitionContentView]; + UIView *snapshotView = [contentView snapshotViewAfterScreenUpdates:true]; + snapshotView.frame = [contentView convertRect:contentView.bounds toView:nil]; +// snapshotView.frame = CGRectOffset([contentView convertRect:contentView.bounds toView:nil], 0.0, -self.view.frame.size.height); + return snapshotView; + } + } + + return nil; +} + - (bool)isFullyOpaque { CGFloat alpha = 0.0f; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index 954b9083f3..f67a281c37 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -224,7 +224,7 @@ CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size sideInset:0.0]; CGFloat y = 0.0; - if (frame.size.width > frame.size.height) { + if (frame.size.width > frame.size.height && !TGIsPad()) { y = edgeInsets.top + frame.size.height; } else { y = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index d011be697b..004b0a73b1 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -74,8 +74,9 @@ enum LegacyMediaPickerGallerySource { case selection(item: TGMediaSelectableItem) } -func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) { +func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController { let reminder = peer?.id == context.account.peerId + let hasSilentPosting = hasSilentPosting && peer?.id != context.account.peerId let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style @@ -191,9 +192,10 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, model.interfaceView.donePressed = { [weak controller] item in if let item = item as? TGMediaPickerGalleryItem { - controller?.dismissWhenReady(animated: true) - completed(item.asset, false, nil) - dismissAll() + completed(item.asset, false, nil, { + controller?.dismissWhenReady(animated: true) + dismissAll() + }) } } model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in @@ -215,23 +217,24 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, model?.dismiss(true, false) } controller.send = { - dismissImpl() - completed(item.asset, false, nil) + completed(item.asset, false, nil, { + dismissImpl() + }) } controller.sendSilently = { - dismissImpl() - completed(item.asset, true, nil) + completed(item.asset, true, nil, { + dismissImpl() + }) } controller.schedule = { presentSchedulePicker(true, { time in - dismissImpl() - completed(item.asset, false, time) + completed(item.asset, false, time, { + dismissImpl() + }) }) } controller.sendWithTimer = { presentTimerPicker { time in - dismissImpl() - var items = selectionContext.selectedItems() ?? [] items.append(item.asset as Any) @@ -239,7 +242,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, editingContext?.setTimer(time as NSNumber, for: item) } - completed(item.asset, false, nil) + completed(item.asset, false, nil, { + dismissImpl() + }) } } controller.customDismissBlock = { [weak legacySheetController] in @@ -288,4 +293,6 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, } } present(legacyController, nil) + + return controller } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 157d84fa88..ba947c120f 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -159,13 +159,19 @@ final class MediaPickerGridItemNode: GridItemNode { let wasHidden = self.isHidden self.isHidden = self.interaction?.hiddenMediaId == asset.localIdentifier if !self.isHidden && wasHidden { - self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.animateFadeIn(animateCheckNode: true) } } } + + func animateFadeIn(animateCheckNode: Bool) { + if animateCheckNode { + self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } override func didLoad() { super.didLoad() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift b/submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift index e5b71fcd2b..f73f26c503 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift @@ -105,7 +105,9 @@ final class MediaPickerMoreButtonNode: ASDisplayNode { guard let strongSelf = self else { return } - strongSelf.action?(strongSelf.contextSourceNode, gesture) + if case .more = strongSelf.iconNode.iconState { + strongSelf.action?(strongSelf.contextSourceNode, gesture) + } } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index b8388a9cc2..06c5b165ae 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -26,14 +26,14 @@ final class MediaPickerInteraction { let openMedia: (PHFetchResult, Int, UIImage?) -> Void let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Void - let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void + let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void let schedule: () -> Void let dismissInput: () -> Void let selectionState: TGMediaSelectionContext? let editingState: TGMediaEditingContext var hiddenMediaId: String? - init(openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { + init(openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.openMedia = openMedia self.openSelectedMedia = openSelectedMedia self.toggleSelection = toggleSelection @@ -127,7 +127,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var presentWebSearch: (MediaGroupsScreen) -> Void = { _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } - public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?) -> Void = { _, _, _ in } + public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _ in } public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -532,21 +532,29 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let updated = self.currentDisplayMode != displayMode self.currentDisplayMode = displayMode + self.dismissInput() + if case .selected = displayMode, self.selectionNode == nil, let controller = self.controller { let selectionNode = MediaPickerSelectedListNode(context: controller.context) - selectionNode.layer.allowsGroupOpacity = true selectionNode.alpha = 0.0 + selectionNode.layer.allowsGroupOpacity = true selectionNode.isUserInteractionEnabled = false selectionNode.interaction = self.controller?.interaction selectionNode.getTransitionView = { [weak self] identifier in if let strongSelf = self { - var view: UIView? + var node: MediaPickerGridItemNode? strongSelf.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.asset?.localIdentifier == identifier { - view = itemNode.view + node = itemNode } } - return view + if let node = node { + return (node.view, { [weak node] animateCheckNode in + node?.animateFadeIn(animateCheckNode: animateCheckNode) + }) + } else { + return nil + } } else { return nil } @@ -573,16 +581,18 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if updated { switch displayMode { case .selected: - self.selectionNode?.alpha = 1.0 - self.selectionNode?.animateIn(completion: completion) - self.updateNavigation(transition: .immediate) + self.selectionNode?.animateIn(initiated: { [weak self] in + self?.updateNavigation(transition: .immediate) + }, completion: completion) case .all: self.selectionNode?.animateOut(completion: completion) } } } - var openingMedia = false + private weak var currentGalleryController: TGModernGalleryController? + + private var openingMedia = false fileprivate func openMedia(fetchResult: PHFetchResult, index: Int, immediateThumbnail: UIImage?) { guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout, !self.openingMedia else { return @@ -600,15 +610,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let reversed = controller.collection == nil let index = reversed ? fetchResult.count - index - 1 : index - presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in + self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.gridNode.view }, transitionView: { [weak self] identifier in return self?.transitionView(for: identifier) - }, completed: { [weak self] result, silently, scheduleTime in + }, completed: { [weak self] result, silently, scheduleTime, completion in if let strongSelf = self { - strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false) + strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a) @@ -636,15 +646,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } self.openingMedia = true - presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in + self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.selectionNode?.view }, transitionView: { [weak self] identifier in return self?.transitionView(for: identifier) - }, completed: { [weak self] result, silently, scheduleTime in + }, completed: { [weak self] result, silently, scheduleTime, completion in if let strongSelf = self { - strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false) + strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) @@ -658,7 +668,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }) } - fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool) { + fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, completion: @escaping () -> Void) { var hasHeic = false let allItems = self.controller?.interaction?.selectionState?.selectedItems() ?? [] for item in allItems { @@ -673,8 +683,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: convertToJpeg, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else { return } - self.controller?.legacyCompletion(signals, silently, scheduleTime) - self.controller?.dismiss(animated: animated) + self.controller?.legacyCompletion(signals, silently, scheduleTime, { [weak self] identifier in + return self?.getItemSnapshot(identifier) + }, { [weak self] in + completion() + self?.controller?.dismiss(animated: animated) + }) } if asFile && hasHeic { @@ -687,7 +701,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { proceed(false) } } - + private func openLimitedMediaOptions() { let presentationData = self.presentationData let controller = ActionSheetController(presentationData: self.presentationData) @@ -712,9 +726,33 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.controller?.present(controller, in: .window(.root)) } - private func transitionView(for identifier: String) -> UIView? { + private func getItemSnapshot(_ identifier: String) -> UIView? { + guard let selectionState = self.controller?.interaction?.selectionState else { + return nil + } + + if let galleryController = self.currentGalleryController { + if selectionState.count() > 1 { + return nil + } + + return galleryController.transitionView() + } + + if selectionState.grouping && selectionState.count() > 1 && (self.selectionNode?.alpha ?? 0.0).isZero { + return nil + } + + if let view = self.transitionView(for: identifier, hideSource: true) { + return view + } else { + return nil + } + } + + private func transitionView(for identifier: String, hideSource: Bool = false) -> UIView? { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { - return selectionNode.transitionView(for: identifier) + return selectionNode.transitionView(for: identifier, hideSource: hideSource) } else { var transitionNode: MediaPickerGridItemNode? self.gridNode.forEachItemNode { itemNode in @@ -722,7 +760,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { transitionNode = itemNode } } - return transitionNode?.transitionView() + let transitionView = transitionNode?.transitionView() + if hideSource { + transitionNode?.isHidden = true + } + return transitionView } } @@ -1113,17 +1155,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.showSelectionUndo(item: item, count: Int32(selectionState.savedStateDifference())) } } - }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in + }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState { if let currentItem = currentItem { selectionState.setItem(currentItem, selected: true) } - strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated) + strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, completion: completion) } }, schedule: { [weak self] in if let strongSelf = self { strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.interaction?.sendSelected(nil, false, time, true) + self?.interaction?.sendSelected(nil, false, time, true, {}) }) } }, dismissInput: { [weak self] in @@ -1292,7 +1334,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }, action: { [weak self] _, f in f(.default) - self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true) + self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {}) }))) if selectionCount > 1 { @@ -1382,7 +1424,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext { } func send(silently: Bool, mode: AttachmentMediaPickerSendMode) { - self.interaction?.sendSelected(nil, silently, nil, true) + self.interaction?.sendSelected(nil, silently, nil, true, {}) } func schedule() { @@ -1457,7 +1499,7 @@ private class MediaPickerGridSelectionGesture: UIPanGestureRecognizer { if !self.processing { if abs(translation.y) > 5.0 { self.state = .failed - } else if abs(translation.x) > 4.0 { + } else if abs(translation.x) > 8.0 { self.processing = true self.gridNode?.scrollView.isScrollEnabled = false self.began() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 0f8c8044fc..d623ff5881 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -44,10 +44,16 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } } + private var readyPromise = Promise() + fileprivate var ready: Signal { + return self.readyPromise.get() + } + init(asset: TGMediaAsset, interaction: MediaPickerInteraction?) { self.imageNode = ImageNode() self.imageNode.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true + self.imageNode.animateFirstTransition = false self.asset = asset self.interaction = interaction @@ -111,6 +117,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } } self.imageNode.setSignal(imageSignal) + self.readyPromise.set(self.imageNode.contentReady) } func updateSelectionState() { @@ -203,7 +210,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { }) } - func animateTo(_ view: UIView) { + func animateTo(_ view: UIView, completion: @escaping (Bool) -> Void) { view.alpha = 0.0 let frame = self.frame @@ -216,6 +223,12 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in view?.alpha = 1.0 + var animateCheckNode = false + if let strongSelf = self, let checkNode = strongSelf.checkNode, checkNode.alpha.isZero { + animateCheckNode = true + } + completion(animateCheckNode) + self?.corners = corners self?.updateLayout(size: frame.size, transition: .immediate) @@ -288,6 +301,9 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI private var validLayout: (size: CGSize, insets: UIEdgeInsets, items: [TGMediaSelectableItem], grouped: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners)? + private var didSetReady = false + private var ready = Promise() + init(context: AccountContext) { self.context = context self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: context.sharedContext.immediateExperimentalUISettings.experimentalBackground) @@ -344,35 +360,46 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI }) } - var getTransitionView: (String) -> UIView? = { _ in return nil } + var getTransitionView: (String ) -> (UIView, (Bool) -> Void)? = { _ in return nil } - func animateIn(completion: @escaping () -> Void = {}) { - self.wallpaperBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in - completion() - }) - self.wallpaperBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - - for (_, backgroundNode) in self.backgroundNodes { - backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) - } - - for (identifier, itemNode) in self.itemNodes { - if let transitionView = self.getTransitionView(identifier) { - itemNode.animateFrom(transitionView) - } else { - itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + func animateIn(initiated: @escaping () -> Void, completion: @escaping () -> Void = {}) { + let _ = (self.ready.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return } - } - - if let topNode = self.messageNodes?.first, !topNode.alpha.isZero { - topNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) - topNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - - if let bottomNode = self.messageNodes?.last, !bottomNode.alpha.isZero { - bottomNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) - bottomNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } + + strongSelf.alpha = 1.0 + initiated() + + strongSelf.wallpaperBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in + completion() + }) + strongSelf.wallpaperBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + for (_, backgroundNode) in strongSelf.backgroundNodes { + backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) + } + + for (identifier, itemNode) in strongSelf.itemNodes { + if let (transitionView, _) = strongSelf.getTransitionView(identifier) { + itemNode.animateFrom(transitionView) + } else { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + } + + if let topNode = strongSelf.messageNodes?.first, !topNode.alpha.isZero { + topNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) + topNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + if let bottomNode = strongSelf.messageNodes?.last, !bottomNode.alpha.isZero { + bottomNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) + bottomNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + }) } func animateOut(completion: @escaping () -> Void = {}) { @@ -400,8 +427,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI } for (identifier, itemNode) in self.itemNodes { - if let transitionView = self.getTransitionView(identifier) { - itemNode.animateTo(transitionView) + if let (transitionView, completion) = self.getTransitionView(identifier) { + itemNode.animateTo(transitionView, completion: completion) } } @@ -593,6 +620,19 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI itemSizes.append(asset.dimensions) } } + + if !self.didSetReady { + self.didSetReady = true + + var signals: [Signal] = [] + for (_, itemNode) in self.itemNodes { + signals.append(itemNode.ready) + } + self.ready.set(combineLatest(queue: Queue.mainQueue(), signals) + |> map { _ in + return true + }) + } let boundingSize = CGSize(width: boundingWidth, height: boundingWidth) var groupLayouts: [([(TGMediaSelectableItem, CGRect, MosaicItemPosition)], CGSize)] = [] @@ -812,9 +852,17 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI transition.updateFrame(node: self.scrollNode, frame: bounds) } - func transitionView(for identifier: String) -> UIView? { + func transitionView(for identifier: String, hideSource: Bool) -> UIView? { + if hideSource { + for (_, node) in self.backgroundNodes { + node.alpha = 0.01 + } + } for (_, itemNode) in self.itemNodes { if itemNode.asset.uniqueIdentifier == identifier { + if hideSource { + itemNode.alpha = 0.01 + } return itemNode.transitionView() } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index ac9b620397..690481f8b7 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -47,6 +47,14 @@ public enum EnqueueMessage { return .forward(source: source, grouping: grouping, attributes: attributes, correlationId: value) } } + + public var groupingKey: Int64? { + if case let .message(_, _, _, _, localGroupingKey, _) = self { + return localGroupingKey + } else { + return nil + } + } } private extension EnqueueMessage { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6cdcbcbdac..fae04d0af4 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10357,7 +10357,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let currentFilesController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, buttons: availableTabs) + let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: self.chatLocation, buttons: availableTabs) attachmentController.requestController = { [weak self, weak attachmentController] type, completion in guard let strongSelf = self else { return @@ -10379,11 +10379,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completion(controller, mediaPickerContext) }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self] signals, silentPosting, scheduleTime in + }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in if !inputText.string.isEmpty { self?.clearInputText() } - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) case .file: strongSelf.controllerNavigationDisposable.set(nil) @@ -10619,8 +10619,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } } - self.present(attachmentController, in: .window(.root)) - self.attachmentController = attachmentController + Queue.mainQueue().justDispatch { + self.present(attachmentController, in: .window(.root)) + self.attachmentController = attachmentController + } } private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { @@ -11025,7 +11027,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(actionSheet, in: .window(.root)) } - private func presentMediaPicker(bannedSendMedia: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) { + private func presentMediaPicker(bannedSendMedia: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -11088,8 +11090,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G controller.getCaptionPanelView = { [weak self] in return self?.getCaptionPanelView() } - controller.legacyCompletion = { signals, silently, scheduleTime in - completion(signals, silently, scheduleTime) + controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in + completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) } present(controller, mediaPickerContext) } @@ -11927,25 +11929,80 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var usedCorrelationId: Int64? var mappedMessages: [EnqueueMessage] = [] + var addedTransitions: [(Int64, [String], () -> Void)] = [] + + var groupedCorrelationIds: [Int64: Int64] = [:] + for item in items { var message = item.message if let uniqueId = item.uniqueId { - let correlationId = Int64.random(in: 0 ..< Int64.max) + let correlationId: Int64 + var addTransition = true + if let groupingKey = message.groupingKey { + if let existing = groupedCorrelationIds[groupingKey] { + correlationId = existing + addTransition = false + } else { + correlationId = Int64.random(in: 0 ..< Int64.max) + groupedCorrelationIds[groupingKey] = correlationId + } + } else { + correlationId = Int64.random(in: 0 ..< Int64.max) + } message = message.withUpdatedCorrelationId(correlationId) - if items.count == 1, let getAnimatedTransitionSource = getAnimatedTransitionSource { - usedCorrelationId = correlationId - completionImpl = nil - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { - return getAnimatedTransitionSource(uniqueId) - })), initiated: { - completion() - }) + if addTransition { + addedTransitions.append((correlationId, [uniqueId], addedTransitions.isEmpty ? completion : {})) + } else { + if let index = addedTransitions.firstIndex(where: { $0.0 == correlationId }) { + var (correlationId, uniqueIds, completion) = addedTransitions[index] + uniqueIds.append(uniqueId) + addedTransitions[index] = (correlationId, uniqueIds, completion) + } } + + usedCorrelationId = correlationId + completionImpl = nil } mappedMessages.append(message) } - + + if addedTransitions.count > 1 { + var transitions: [(Int64, ChatMessageTransitionNode.Source, () -> Void)] = [] + for (correlationId, uniqueIds, initiated) in addedTransitions { + var source: ChatMessageTransitionNode.Source? + if uniqueIds.count > 1 { + source = .groupedMediaInput(ChatMessageTransitionNode.Source.GroupedMediaInput(extractSnapshots: { + return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) + })) + } else if let uniqueId = uniqueIds.first { + source = .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { + return getAnimatedTransitionSource?(uniqueId) + })) + } + if let source = source { + transitions.append((correlationId, source, initiated)) + } + } + strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) + } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { + var source: ChatMessageTransitionNode.Source? + if uniqueIds.count > 1 { + source = .groupedMediaInput(ChatMessageTransitionNode.Source.GroupedMediaInput(extractSnapshots: { + return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) + })) + } else if let uniqueId = uniqueIds.first { + source = .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { + return getAnimatedTransitionSource?(uniqueId) + })) + } + if let source = source { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { + initiated() + }) + } + } + let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 0b902a0b54..006c84467c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2595,15 +2595,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self?.scrolledToSomeIndex?() } - if let currentSendAnimationCorrelationId = strongSelf.currentSendAnimationCorrelationId { - var foundItemNode: ChatMessageItemView? + if let currentSendAnimationCorrelationIds = strongSelf.currentSendAnimationCorrelationIds { + var foundItemNodes: [Int64: ChatMessageItemView] = [:] strongSelf.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { for (message, _) in item.content { for attribute in message.attributes { - if let attribute = attribute as? OutgoingMessageInfoAttribute { - if attribute.correlationId == currentSendAnimationCorrelationId { - foundItemNode = itemNode + if let attribute = attribute as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { + if currentSendAnimationCorrelationIds.contains(correlationId) { + foundItemNodes[correlationId] = itemNode } } } @@ -2611,9 +2611,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - if let foundItemNode = foundItemNode { - strongSelf.currentSendAnimationCorrelationId = nil - strongSelf.animationCorrelationMessageFound?(foundItemNode, currentSendAnimationCorrelationId) + if !foundItemNodes.isEmpty { + strongSelf.currentSendAnimationCorrelationIds = nil + strongSelf.animationCorrelationMessagesFound?(foundItemNodes) } } @@ -3225,12 +3225,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - private var currentSendAnimationCorrelationId: Int64? - func setCurrentSendAnimationCorrelationId(_ value: Int64?) { - self.currentSendAnimationCorrelationId = value + private var currentSendAnimationCorrelationIds: Set? + func setCurrentSendAnimationCorrelationIds(_ value: Set?) { + self.currentSendAnimationCorrelationIds = value } - var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)? + var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)? final class SnapshotState { fileprivate let snapshotTopInset: CGFloat diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 2e2ec3b160..dc62b3c9a8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -799,6 +799,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.mainContextSourceNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } + func animateContentFromGroupedMediaInput(transition: CombinedTransition) -> [CGRect] { + self.mainContextSourceNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + var rects: [CGRect] = [] + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageMediaBubbleContentNode { + rects.append(contentNode.frame.offsetBy(dx: -76.0, dy: 0.0)) + } + } + return rects + } + override func didLoad() { super.didLoad() diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index e07b8d4399..1b8c571cbe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -169,12 +169,21 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.extractSnapshot = extractSnapshot } } + + final class GroupedMediaInput { + let extractSnapshots: () -> [UIView] + + init(extractSnapshots: @escaping () -> [UIView]) { + self.extractSnapshots = extractSnapshots + } + } case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?) case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?) case audioMicInput(AudioMicInput) case videoMessage(VideoMessage) case mediaInput(MediaInput) + case groupedMediaInput(GroupedMediaInput) } final class DecorationItemNode: ASDisplayNode { @@ -556,6 +565,92 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) } + } else { + self.endAnimation() + } + case let .groupedMediaInput(groupedMediaInput): + let snapshotViews = groupedMediaInput.extractSnapshots() + if snapshotViews.isEmpty { + self.endAnimation() + return + } + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + itemNode.cancelInsertionAnimations() + + self.contextSourceNode.isExtractedToContextPreview = true + self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) + + self.containerNode.addSubnode(self.contextSourceNode.contentNode) + + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + + var targetContentRects: [CGRect] = [] + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + targetContentRects = itemNode.animateContentFromGroupedMediaInput(transition: combinedTransition) + } + + let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) + + func boundingRect(for views: [UIView]) -> CGRect { + var minX: CGFloat = .greatestFiniteMagnitude + var minY: CGFloat = .greatestFiniteMagnitude + var maxX: CGFloat = .leastNonzeroMagnitude + var maxY: CGFloat = .leastNonzeroMagnitude + + for view in views { + let rect = view.frame + if rect.minX < minX { + minX = rect.minX + } + if rect.minY < minY { + minY = rect.minY + } + if rect.maxX > maxX { + maxX = rect.maxX + } + if rect.maxY > maxY { + maxY = rect.maxY + } + } + return CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY)) + } + + let sourceBackgroundAbsoluteRect = boundingRect(for: snapshotViews) + let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size) + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) + + self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.endAnimation() + }) + + combinedTransition.horizontal.animateTransformScale(node: self.contextSourceNode.contentNode, from: CGPoint(x: sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width, y: sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height)) + + var index = 0 + for snapshotView in snapshotViews { + let targetContentRect = targetContentRects[index] + let targetAbsoluteContentRect = targetContentRect.offsetBy(dx: targetAbsoluteRect.minX, dy: targetAbsoluteRect.minY) + + snapshotView.center = targetAbsoluteContentRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY) + self.containerNode.view.addSubview(snapshotView) + + combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (snapshotView.frame.width / targetContentRect.width), y: 1.0 / (snapshotView.frame.height / targetContentRect.height))) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + index += 1 + } + + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) } } } @@ -652,14 +747,14 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private let getContentAreaInScreenSpace: () -> CGRect private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void - private var currentPendingItem: (Int64, Source, () -> Void)? + private var currentPendingItems: [Int64: (Source, () -> Void)] = [:] private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNode] = [] private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { - return self.currentPendingItem != nil + return !self.currentPendingItems.isEmpty } var hasOngoingTransitions: Bool { @@ -673,21 +768,39 @@ public final class ChatMessageTransitionNode: ASDisplayNode { super.init() - self.listNode.animationCorrelationMessageFound = { [weak self] itemNode, correlationId in - guard let strongSelf = self, let (currentId, currentSource, initiated) = strongSelf.currentPendingItem else { + self.listNode.animationCorrelationMessagesFound = { [weak self] itemNodeAndCorrelationIds in + guard let strongSelf = self else { return } - if currentId == correlationId { - strongSelf.currentPendingItem = nil - strongSelf.beginAnimation(itemNode: itemNode, source: currentSource) - initiated() + + for (correlationId, itemNode) in itemNodeAndCorrelationIds { + if let (currentSource, initiated) = strongSelf.currentPendingItems[correlationId] { + strongSelf.beginAnimation(itemNode: itemNode, source: currentSource) + initiated() + } + } + + if itemNodeAndCorrelationIds.count == strongSelf.currentPendingItems.count { + strongSelf.currentPendingItems = [:] } } } func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) { - self.currentPendingItem = (correlationId, source, initiated) - self.listNode.setCurrentSendAnimationCorrelationId(correlationId) + self.currentPendingItems = [correlationId: (source, initiated)] + self.listNode.setCurrentSendAnimationCorrelationIds(Set([correlationId])) + } + + func add(grouped: [(correlationId: Int64, source: Source, initiated: () -> Void)]) { + var currentPendingItems: [Int64: (Source, () -> Void)] = [:] + var correlationIds = Set() + for (correlationId, source, initiated) in grouped { + currentPendingItems[correlationId] = (source, initiated) + correlationIds.insert(correlationId) + } + + self.currentPendingItems = currentPendingItems + self.listNode.setCurrentSendAnimationCorrelationIds(correlationIds) } func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode { @@ -730,7 +843,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.animatingItemNodes.append(animatingItemNode) switch source { - case .audioMicInput, .videoMessage, .mediaInput: + case .audioMicInput, .videoMessage, .mediaInput, .groupedMediaInput: let overlayController = OverlayTransitionContainerController() overlayController.displayNode.addSubnode(animatingItemNode) animatingItemNode.overlayController = overlayController