Attachment menu improvements

This commit is contained in:
Ilya Laktyushin 2022-03-08 20:44:45 +04:00
parent 8f560ba10b
commit 8f44a8fee6
19 changed files with 461 additions and 130 deletions

View File

@ -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

View File

@ -90,6 +90,7 @@ private func generateMaskImage() -> UIImage? {
public class AttachmentController: ViewController {
private let context: AccountContext
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
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<PresentationData, NoError>)? = nil, buttons: [AttachmentButtonType]) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType]) {
self.context = context
self.buttons = buttons
self.updatedPresentationData = updatedPresentationData
self.chatLocation = chatLocation
self.buttons = buttons
super.init(navigationBarPresentationData: nil)

View File

@ -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<PresentationData, NoError>)?) {
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
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

View File

@ -40,6 +40,8 @@ typedef NS_ENUM(NSUInteger, TGModernGalleryScrollAnimationDirection) {
- (TGModernGalleryItemView *)itemViewForItem:(id<TGModernGalleryItem>)item;
- (id<TGModernGalleryItem>)currentItem;
- (UIView *)transitionView;
- (void)setCurrentItemIndex:(NSUInteger)index animated:(bool)animated;
- (void)setCurrentItemIndex:(NSUInteger)index direction:(TGModernGalleryScrollAnimationDirection)direction animated:(bool)animated;

View File

@ -54,6 +54,7 @@
- (UIView *)footerView;
- (UIView *)transitionView;
- (UIView *)transitionContentView;
- (CGRect)transitionViewContentRect;
- (bool)dismissControllerNowOrSchedule;

View File

@ -9,7 +9,6 @@
- (CGSize)contentSize;
- (UIView *)contentView;
- (UIView *)transitionContentView;
- (void)reset;

View File

@ -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)

View File

@ -178,6 +178,27 @@
}
}
- (UIView *)transitionView {
id<TGModernGalleryItem> 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;

View File

@ -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);

View File

@ -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 {
completed(item.asset, false, nil, {
controller?.dismissWhenReady(animated: true)
completed(item.asset, false, nil)
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 = {
completed(item.asset, false, nil, {
dismissImpl()
completed(item.asset, false, nil)
})
}
controller.sendSilently = {
completed(item.asset, true, nil, {
dismissImpl()
completed(item.asset, true, nil)
})
}
controller.schedule = {
presentSchedulePicker(true, { time in
completed(item.asset, false, time, {
dismissImpl()
completed(item.asset, false, time)
})
})
}
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
}

View File

@ -159,13 +159,19 @@ final class MediaPickerGridItemNode: GridItemNode {
let wasHidden = self.isHidden
self.isHidden = self.interaction?.hiddenMediaId == asset.localIdentifier
if !self.isHidden && wasHidden {
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()

View File

@ -105,9 +105,11 @@ final class MediaPickerMoreButtonNode: ASDisplayNode {
guard let strongSelf = self else {
return
}
if case .more = strongSelf.iconNode.iconState {
strongSelf.action?(strongSelf.contextSourceNode, gesture)
}
}
}
@objc private func buttonPressed() {
self.action?(self.contextSourceNode, nil)

View File

@ -26,14 +26,14 @@ final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, 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<PHAsset>, 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<PHAsset>, 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<PHAsset>, 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 {
@ -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()

View File

@ -44,10 +44,16 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
}
}
private var readyPromise = Promise<Bool>()
fileprivate var ready: Signal<Bool, NoError> {
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<Bool>()
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
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
}
strongSelf.alpha = 1.0
initiated()
strongSelf.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)
strongSelf.wallpaperBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
for (_, backgroundNode) in self.backgroundNodes {
for (_, backgroundNode) in strongSelf.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) {
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 = self.messageNodes?.first, !topNode.alpha.isZero {
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 = self.messageNodes?.last, !bottomNode.alpha.isZero {
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)
}
}
@ -594,6 +621,19 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
}
}
if !self.didSetReady {
self.didSetReady = true
var signals: [Signal<Bool, NoError>] = []
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)] = []
if grouped && items.count > 1 {
@ -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()
}
}

View File

@ -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 {

View File

@ -10357,7 +10357,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
let currentLocationController = Atomic<AttachmentContainable?>(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,9 +10619,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
}
Queue.mainQueue().justDispatch {
self.present(attachmentController, in: .window(.root))
self.attachmentController = attachmentController
}
}
private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
@ -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 {
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
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: {
return getAnimatedTransitionSource(uniqueId)
})), initiated: {
completion()
})
}
}
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({

View File

@ -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<Int64>?
func setCurrentSendAnimationCorrelationIds(_ value: Set<Int64>?) {
self.currentSendAnimationCorrelationIds = value
}
var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)?
var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)?
final class SnapshotState {
fileprivate let snapshotTopInset: CGFloat

View File

@ -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()

View File

@ -170,11 +170,20 @@ public final class ChatMessageTransitionNode: ASDisplayNode {
}
}
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
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<Int64>()
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