diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 97b818ce4c..35266a4077 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6569,6 +6569,9 @@ Sorry for the inconvenience."; "Gallery.VideoSaved" = "Video Saved"; "Gallery.WaitForVideoDownoad" = "Please wait for the video to be fully downloaded."; +"Gallery.SaveImage" = "Save Image"; +"Gallery.SaveVideo" = "Save Video"; + "VoiceChat.VideoParticipantsLimitExceededExtended" = "The voice chat is over %@ members.\nNew participants only have access to audio stream. "; "PlaybackSpeed.Title" = "Playback Speed"; diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 2dda65089b..4f186d65d9 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -869,6 +869,7 @@ public final class AnimatedStickerNode: ASDisplayNode { self.timer.swap(nil)?.invalidate() } + private weak var nodeToCopyFrameFrom: AnimatedStickerNode? override public func didLoad() { super.didLoad() @@ -879,8 +880,22 @@ public final class AnimatedStickerNode: ASDisplayNode { //self.renderer = MetalAnimationRenderer() #endif self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + if let contents = self.nodeToCopyFrameFrom?.renderer?.contents { + self.renderer?.contents = contents + } + self.nodeToCopyFrameFrom = nil self.addSubnode(self.renderer!) } + + public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) { + if let renderer = self.renderer { + if let contents = otherNode?.renderer?.contents { + renderer.contents = contents + } + } else { + self.nodeToCopyFrameFrom = otherNode + } + } public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) { if width < 2 || height < 2 { @@ -973,7 +988,7 @@ public final class AnimatedStickerNode: ASDisplayNode { } private var isSetUpForPlayback = false - + public func play(firstFrame: Bool = false, fromIndex: Int? = nil) { switch self.playbackMode { case .once: diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 22e53c3068..f8882bc6ae 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -300,6 +300,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private var currentGeneralScrollDirection: GeneralScrollDirection? public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in } + public private(set) var isReordering = false + public final var willBeginReorder: (CGPoint) -> Void = { _ in } + public final var reorderBegan: () -> Void = { } public final var reorderItem: (Int, Int, Any?) -> Signal = { _, _, _ in return .single(false) } public final var reorderCompleted: (Any?) -> Void = { _ in } @@ -322,8 +325,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private var reorderNode: ListViewReorderingItemNode? private var reorderFeedback: HapticFeedback? private var reorderFeedbackDisposable: MetaDisposable? - private var isReorderingItems: Bool = false + private var reorderInProgress: Bool = false private var reorderingItemsCompleted: (() -> Void)? + public var reorderedItemHasShadow = true private let waitingForNodesDisposable = MetaDisposable() @@ -394,15 +398,19 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let itemNodeFrame = itemNode.frame let itemNodeBounds = itemNode.bounds if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) { - strongSelf.beginReordering(itemNode: itemNode) - return true + let requiresLongPress = !strongSelf.reorderedItemHasShadow + return (true, requiresLongPress, itemNode) } break } } } } - return false + return (false, false, nil) + }, willBegin: { [weak self] point in + self?.willBeginReorder(point) + }, began: { [weak self] itemNode in + self?.beginReordering(itemNode: itemNode) }, ended: { [weak self] in self?.endReordering() }, moved: { [weak self] offset in @@ -469,10 +477,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } private func beginReordering(itemNode: ListViewItemNode) { + self.isReordering = true + self.reorderBegan() + if let reorderNode = self.reorderNode { reorderNode.removeFromSupernode() } - let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) + let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin, hasShadow: self.reorderedItemHasShadow) self.reorderNode = reorderNode if let verticalScrollIndicator = self.verticalScrollIndicator { self.insertSubnode(reorderNode, belowSubnode: verticalScrollIndicator) @@ -509,8 +520,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } strongSelf.reorderCompleted(strongSelf.opaqueTransactionState) + strongSelf.isReordering = false } - if self.isReorderingItems { + if self.reorderInProgress { self.reorderingItemsCompleted = f } else { f() @@ -587,14 +599,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if self.reorderFeedbackDisposable == nil { self.reorderFeedbackDisposable = MetaDisposable() } - self.isReorderingItems = true + self.reorderInProgress = true self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState) |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } - strongSelf.isReorderingItems = false + strongSelf.reorderInProgress = false if let reorderingItemsCompleted = strongSelf.reorderingItemsCompleted { strongSelf.reorderingItemsCompleted = nil reorderingItemsCompleted() diff --git a/submodules/Display/Source/ListViewReorderingGestureRecognizer.swift b/submodules/Display/Source/ListViewReorderingGestureRecognizer.swift index 22748518c5..9a12c39c12 100644 --- a/submodules/Display/Source/ListViewReorderingGestureRecognizer.swift +++ b/submodules/Display/Source/ListViewReorderingGestureRecognizer.swift @@ -1,28 +1,101 @@ import Foundation import UIKit +import SwiftSignalKit -final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { - private let shouldBegin: (CGPoint) -> Bool +public final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?) + private let willBegin: (CGPoint) -> Void + private let began: (ListViewItemNode) -> Void private let ended: () -> Void private let moved: (CGFloat) -> Void private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? - init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { + private var itemNode: ListViewItemNode? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (ListViewItemNode) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began self.ended = ended self.moved = moved super.init(target: nil, action: nil) } - override func reset() { + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemNode = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.8, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemNode = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { super.reset() + self.itemNode = nil + self.stopLongTapTimer() + self.stopLongPressTimer() self.initialLocation = nil } - override func touchesBegan(_ touches: Set, with event: UIEvent) { + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let itemNode = self.itemNode { + self.began(itemNode) + } + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if self.numberOfTouches > 1 { @@ -32,40 +105,76 @@ final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { } if self.state == .possible { - if let location = touches.first?.location(in: self.view), self.shouldBegin(location) { - self.initialLocation = location - self.state = .began + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location) + if allowed { + self.itemNode = itemNode + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + } + } else { + self.state = .failed + } } else { self.state = .failed } } } - override func touchesEnded(_ touches: Set, with event: UIEvent) { + override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.state = .failed + } if self.state == .began || self.state == .changed { self.ended() self.state = .failed } } - override func touchesCancelled(_ touches: Set, with event: UIEvent) { + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.state = .failed + } if self.state == .began || self.state == .changed { self.ended() self.state = .failed } } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { self.state = .changed let offset = location.y - initialLocation.y self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.state = .failed + } } } } diff --git a/submodules/Display/Source/ListViewReorderingItemNode.swift b/submodules/Display/Source/ListViewReorderingItemNode.swift index 8d72a524ba..b1dd58b192 100644 --- a/submodules/Display/Source/ListViewReorderingItemNode.swift +++ b/submodules/Display/Source/ListViewReorderingItemNode.swift @@ -25,14 +25,16 @@ private final class CopyView: UIView { let topShadow: UIImageView let bottomShadow: UIImageView - override init(frame: CGRect) { + init(frame: CGRect, hasShadow: Bool) { self.topShadow = UIImageView() self.bottomShadow = UIImageView() super.init(frame: frame) - self.topShadow.image = generateShadowImage(mirror: true) - self.bottomShadow.image = generateShadowImage(mirror: false) + if hasShadow { + self.topShadow.image = generateShadowImage(mirror: true) + self.bottomShadow.image = generateShadowImage(mirror: false) + } self.addSubview(self.topShadow) self.addSubview(self.bottomShadow) @@ -51,9 +53,9 @@ final class ListViewReorderingItemNode: ASDisplayNode { private let copyView: CopyView private let initialLocation: CGPoint - init(itemNode: ListViewItemNode, initialLocation: CGPoint) { + init(itemNode: ListViewItemNode, initialLocation: CGPoint, hasShadow: Bool) { self.itemNode = itemNode - self.copyView = CopyView(frame: CGRect()) + self.copyView = CopyView(frame: CGRect(), hasShadow: hasShadow) let snapshotView = itemNode.snapshotForReordering() self.initialLocation = initialLocation @@ -68,8 +70,6 @@ final class ListViewReorderingItemNode: ASDisplayNode { self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size) self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -30.0), size: CGSize(width: copyView.bounds.size.width, height: 45.0)) - - self.copyView.bottomShadow.image = generateShadowImage(mirror: false) self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 45.0)) self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 56578535cd..473341b85c 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2089,7 +2089,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { c.setItems(strongSelf.contextMenuSpeedItems()) }))) if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveToGallery, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) if let strongSelf = self { diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 62c62c5be4..8535b77020 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -387,20 +387,22 @@ public final class ShareController: ViewController { break case let .image(representations): if case .saveToCameraRoll = preferredAction { - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Gallery_SaveImage, action: { [weak self] in self?.saveToCameraRoll(representations: representations) self?.actionCompleted?() }) } case let .media(mediaReference): var canSave = false + var isVideo = false if mediaReference.media is TelegramMediaImage { canSave = true - } else if mediaReference.media is TelegramMediaFile { + } else if let file = mediaReference.media as? TelegramMediaFile { canSave = true + isVideo = file.isVideo } if case .saveToCameraRoll = preferredAction, canSave { - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self.defaultAction = ShareControllerAction(title: isVideo ? self.presentationData.strings.Gallery_SaveVideo : self.presentationData.strings.Gallery_SaveImage, action: { [weak self] in self?.saveToCameraRoll(mediaReference: mediaReference) self?.actionCompleted?() }) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 56fce91c05..04ff60d6e8 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -955,8 +955,6 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } if let status = status { contentNode.state = .progress(status) - } else { - } }, completed: { [weak self] in guard let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareLoadingContainerNode else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index fde14f3b61..6d2f942849 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -615,17 +615,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } if resourceAvailable, !message.containsSecretMedia { var mediaReference: AnyMediaReference? + var isVideo = false for media in message.media { if let image = media as? TelegramMediaImage, let _ = largestImageRepresentation(image.representations) { mediaReference = ImageMediaReference.standalone(media: image).abstract break } else if let file = media as? TelegramMediaFile, file.isVideo { mediaReference = FileMediaReference.standalone(media: file).abstract + isVideo = true break } } if let mediaReference = mediaReference { - actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Preview_SaveToCameraRoll, icon: { theme in + actions.append(.action(ContextMenuActionItem(text: isVideo ? chatPresentationInterfaceState.strings.Gallery_SaveVideo : chatPresentationInterfaceState.strings.Gallery_SaveImage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: mediaReference) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index aabb9398f6..f3210da8b5 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -6,6 +6,8 @@ import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData +import AnimatedStickerNode +import TelegramAnimatedStickerNode enum ChatMediaInputMetaSectionItemType: Equatable { case savedStickers @@ -13,10 +15,11 @@ enum ChatMediaInputMetaSectionItemType: Equatable { case stickersMode case savedGifs case trendingGifs - case gifEmoji(String) + case gifEmoji(String, TelegramMediaFile?) } final class ChatMediaInputMetaSectionItem: ListViewItem { + let account: Account let inputNodeInteraction: ChatMediaInputNodeInteraction let type: ChatMediaInputMetaSectionItemType let theme: PresentationTheme @@ -27,7 +30,8 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { + self.account = account self.inputNodeInteraction = inputNodeInteraction self.type = type self.selectedItem = selected @@ -41,7 +45,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { Queue.mainQueue().async { node.inputNodeInteraction = self.inputNodeInteraction node.setItem(item: self) - node.updateTheme(theme: self.theme, expanded: self.expanded) + node.updateTheme(account: self.account, theme: self.theme, expanded: self.expanded) node.updateIsHighlighted() node.updateAppearanceTransition(transition: .immediate) @@ -61,7 +65,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { Queue.mainQueue().async { completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in (node() as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self) - (node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme, expanded: self.expanded) + (node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(account: self.account, theme: self.theme, expanded: self.expanded) }) } } @@ -86,6 +90,8 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { private let highlightNode: ASImageNode private let titleNode: ImmediateTextNode + private var animatedStickerNode: AnimatedStickerNode? + private var currentExpanded = false var item: ChatMediaInputMetaSectionItem? @@ -94,6 +100,23 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { var theme: PresentationTheme? + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false + self.animatedStickerNode?.visibility = self.visibilityStatus && loopAnimatedStickers + } + } + } + + private let stickerFetchedDisposable = MetaDisposable() + init() { self.containerNode = ASDisplayNode() self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -130,6 +153,10 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.scalingNode.addSubnode(self.textNodeContainer) } + deinit { + self.stickerFetchedDisposable.dispose() + } + override func didLoad() { super.didLoad() } @@ -146,7 +173,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { } } - func updateTheme(theme: PresentationTheme, expanded: Bool) { + func updateTheme(account: Account, theme: PresentationTheme, expanded: Bool) { let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) @@ -174,7 +201,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { case .trendingGifs: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme) title = "Trending" - case let .gifEmoji(emoji): + case let .gifEmoji(emoji, file): var emoji = emoji switch emoji { case "😡": @@ -200,16 +227,38 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { default: break } - if emoji == "🥳" { - if #available(iOSApplicationExtension 12.1, iOS 12.1, *) { - } else { - emoji = "🎉" - } - } +// if emoji == "🥳" { +// if #available(iOSApplicationExtension 12.1, iOS 12.1, *) { +// } else { +// emoji = "🎉" +// } +// } self.imageNode.image = nil - self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black) - let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) - self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize) + + if let file = file { + + let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false + let animatedStickerNode: AnimatedStickerNode + if let current = self.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + self.animatedStickerNode = animatedStickerNode + // if let placeholderNode = self.placeholderNode { + // self.scalingNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode) + // } else { + self.scalingNode.addSubnode(animatedStickerNode) + // } + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: file.resource), width: 128, height: 128, mode: .cached) + } + animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers + + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + } else { + self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black) + let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize) + } } } self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) @@ -224,7 +273,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width, height: expandedBoundingSize.height)) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 6.0), size: titleSize) let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) @@ -236,6 +285,11 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.currentExpanded = expanded + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.frame = self.imageNode.frame + animatedStickerNode.updateLayout(size: self.imageNode.frame.size) + } + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize)) } @@ -256,7 +310,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { if case .trending = inputNodeInteraction.highlightedGifMode { isHighlighted = true } - case let .gifEmoji(emoji): + case let .gifEmoji(emoji, _): if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode { isHighlighted = true } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 0500b8ced2..385e59d3b8 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -30,11 +30,20 @@ enum CanInstallPeerSpecificPack { case available(peer: Peer, dismissed: Bool) } +final class ChatMediaInputPanelOpaqueState { + let entries: [ChatMediaInputPanelEntry] + + init(entries: [ChatMediaInputPanelEntry]) { + self.entries = entries + } +} + struct ChatMediaInputPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let scrollToItem: ListViewScrollToItem? + let updateOpaqueState: ChatMediaInputPanelOpaqueState? } struct ChatMediaInputGridTransition { @@ -55,7 +64,7 @@ func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fr let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } - return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem) + return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem, updateOpaqueState: ChatMediaInputPanelOpaqueState(entries: toEntries)) } func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> ChatMediaInputGridTransition { @@ -153,7 +162,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } -func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, temporaryPackOrder: [ItemCollectionId]? = nil, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] if hasGifs { entries.append(.recentGifs(theme, expanded)) @@ -189,13 +198,36 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } var index = 0 - for (_, info, item) in view.collectionInfos { - if let info = info as? StickerPackCollectionInfo, item != nil { - entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme, expanded: expanded)) - index += 1 + + var sortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = [] + for (id, info, item) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo, let item = item as? StickerPackItem { + sortedPacks.append((id, info, item)) } } + if let temporaryPackOrder = temporaryPackOrder { + var packDict: [ItemCollectionId: Int] = [:] + for i in 0 ..< sortedPacks.count { + packDict[sortedPacks[i].0] = i + } + var tempSortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = [] + var processedPacks = Set() + for id in temporaryPackOrder { + if let index = packDict[id] { + tempSortedPacks.append(sortedPacks[index]) + processedPacks.insert(id) + } + } + let restPacks = sortedPacks.filter { !processedPacks.contains($0.0) } + sortedPacks = restPacks + tempSortedPacks + } + + for (_, info, topItem) in sortedPacks { + entries.append(.stickerPack(index: index, info: info, topItem: topItem, theme: theme, expanded: expanded)) + index += 1 + } + if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack { entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } @@ -206,14 +238,14 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere return entries } -func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], expanded: Bool) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], animatedEmojiStickers: [String: [StickerPackItem]], expanded: Bool) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.stickersMode(theme, expanded)) entries.append(.savedGifs(theme, expanded)) entries.append(.trendingGifs(theme, expanded)) for reaction in reactions { - entries.append(.gifEmotion(entries.count, theme, reaction, expanded)) + entries.append(.gifEmotion(entries.count, theme, reaction, animatedEmojiStickers[reaction]?.first?.file, expanded)) } return entries @@ -248,7 +280,7 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered } var trendingIsDismissed = false - if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, trendingPacks.map({ $0.info.id.id }) == dismissedTrendingStickerPacks { + if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, Set(trendingPacks.map({ $0.info.id.id })) == Set(dismissedTrendingStickerPacks) { trendingIsDismissed = true } if !trendingIsDismissed { @@ -470,6 +502,7 @@ final class ChatMediaInputNode: ChatInputNode { } } private var panelFocusTimer: SwiftSignalKit.Timer? + private var lastReorderItemIndex: Int? var requestDisableStickerAnimations: ((Bool) -> Void)? @@ -514,6 +547,7 @@ final class ChatMediaInputNode: ChatInputNode { self.listView = ListView() self.listView.useSingleDimensionTouchPoint = true + self.listView.reorderedItemHasShadow = false self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.listView.accessibilityPageScrolledString = { row, count in @@ -551,6 +585,147 @@ final class ChatMediaInputNode: ChatInputNode { super.init() + let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil) + + self.listView.willBeginReorder = { [weak self] point in + self?.listView.beganInteractiveDragging(point) + } + + self.listView.reorderBegan = { [weak self] in + self?.stopCollapseTimer() + } + + self.listView.reorderItem = { [weak self] fromIndex, toIndex, opaqueState in + guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else { + return .single(false) + } + self?.lastReorderItemIndex = toIndex + + let fromEntry = entries[fromIndex] + guard case let .stickerPack(_, fromPackInfo, _, _, _) = fromEntry else { + return .single(false) + } + var referenceId: ItemCollectionId? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .stickerPack(_, toPackInfo, _, _, _): + referenceId = toPackInfo.id + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + var currentIds: [ItemCollectionId] = [] + for entry in entries { + switch entry { + case let .stickerPack(_, info, _, _, _): + currentIds.append(info.id) + default: + break + } + } + + var previousIndex: Int? + for i in 0 ..< currentIds.count { + if currentIds[i] == fromPackInfo.id { + previousIndex = i + currentIds.remove(at: i) + break + } + } + + var didReorder = false + + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< currentIds.count { + if currentIds[i] == referenceId { + if fromIndex < toIndex { + didReorder = previousIndex != i + 1 + currentIds.insert(fromPackInfo.id, at: i + 1) + } else { + didReorder = previousIndex != i + currentIds.insert(fromPackInfo.id, at: i) + } + inserted = true + break + } + } + if !inserted { + didReorder = previousIndex != currentIds.count + currentIds.append(fromPackInfo.id) + } + } else if beforeAll { + didReorder = previousIndex != 0 + currentIds.insert(fromPackInfo.id, at: 0) + } else if afterAll { + didReorder = previousIndex != currentIds.count + currentIds.append(fromPackInfo.id) + } + + temporaryPackOrder.set(.single(currentIds)) + + return .single(didReorder) + } + self.listView.reorderCompleted = { [weak self] opaqueState in + guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else { + return + } + + var currentIds: [ItemCollectionId] = [] + for entry in entries { + switch entry { + case let .stickerPack(_, info, _, _, _): + currentIds.append(info.id) + default: + break + } + } + let _ = (context.account.postbox.transaction { transaction -> Void in + let namespace = Namespaces.ItemCollection.CloudStickerPacks + let infos = transaction.getItemCollectionsInfos(namespace: namespace) + + var packDict: [ItemCollectionId: Int] = [:] + for i in 0 ..< infos.count { + packDict[infos[i].0] = i + } + var tempSortedPacks: [(ItemCollectionId, ItemCollectionInfo)] = [] + var processedPacks = Set() + for id in currentIds { + if let index = packDict[id] { + tempSortedPacks.append(infos[index]) + processedPacks.insert(id) + } + } + let restPacks = infos.filter { !processedPacks.contains($0.0) } + let sortedPacks = restPacks + tempSortedPacks + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .sync, noDelay: false) + transaction.replaceItemCollectionInfos(namespace: namespace, itemCollectionInfos: sortedPacks) + } + |> deliverOnMainQueue).start(completed: { [weak self] in + temporaryPackOrder.set(.single(nil)) + + if let strongSelf = self { + if let lastReorderItemIndex = strongSelf.lastReorderItemIndex { + strongSelf.lastReorderItemIndex = nil + if strongSelf.panelIsFocused { + strongSelf.panelFocusScrollToIndex = lastReorderItemIndex + } + } + } + + self?.startCollapseTimer(timeout: 1.0) + }) + } + self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) { var index: Int32 = 0 @@ -885,10 +1060,30 @@ final class ChatMediaInputNode: ChatInputNode { } |> distinctUntilChanged + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for case let item as StickerPackItem in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + let previousView = Atomic(value: nil) let transitionQueue = Queue() - let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelIsFocusedPromise.get(), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager)) - |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelIsFocusedPromise.get(), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), temporaryPackOrder.get(), animatedEmojiStickers) + |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks, temporaryPackOrder, animatedEmojiStickers -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate @@ -912,8 +1107,8 @@ final class ChatMediaInputNode: ChatInputNode { installedPacks.insert(info.0) } - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, expanded: panelExpanded) - let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, expanded: panelExpanded) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, temporaryPackOrder: temporaryPackOrder, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, expanded: panelExpanded) + let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, animatedEmojiStickers: animatedEmojiStickers, expanded: panelExpanded) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, trendingPacks: trendingPacks, dismissedTrendingStickerPacks: dismissedTrendingStickerPacks, strings: strings, theme: theme) if view.higher == nil { @@ -1017,7 +1212,8 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.beganInteractiveDragging = { [weak self] position in if let strongSelf = self { - strongSelf.panelFocusTimer?.invalidate() + strongSelf.stopCollapseTimer() + var position = position var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) if index == nil { @@ -1050,13 +1246,13 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.panelFocusScrollToIndex = nil strongSelf.panelFocusInitialPosition = nil } - strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5) + strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 1.5) } } self.gifListView.beganInteractiveDragging = { [weak self] position in if let strongSelf = self { - strongSelf.panelFocusTimer?.invalidate() + strongSelf.stopCollapseTimer() var position = position var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) if index == nil { @@ -1088,7 +1284,7 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.panelFocusScrollToIndex = nil strongSelf.panelFocusInitialPosition = nil } - strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5) + strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 1.5) } } } @@ -1108,7 +1304,7 @@ final class ChatMediaInputNode: ChatInputNode { self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring)) } - private func setupCollapseTimer(timeout: Double) { + private func startCollapseTimer(timeout: Double) { self.panelFocusTimer?.invalidate() let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in @@ -1124,6 +1320,11 @@ final class ChatMediaInputNode: ChatInputNode { timer.start() } + private func stopCollapseTimer() { + self.panelFocusTimer?.invalidate() + self.panelFocusTimer = nil + } + private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { @@ -2024,7 +2225,7 @@ final class ChatMediaInputNode: ChatInputNode { } var scrollToItem: ListViewScrollToItem? - if let targetIndex = self.panelFocusScrollToIndex { + if let targetIndex = self.panelFocusScrollToIndex, !self.listView.isReordering { var position: ListViewScrollPosition if self.panelIsFocused { if let initialPosition = self.panelFocusInitialPosition { @@ -2044,7 +2245,7 @@ final class ChatMediaInputNode: ChatInputNode { scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: nil, completion: { [weak self] _ in + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: transition.updateOpaqueState, completion: { [weak self] _ in if let strongSelf = self { strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) if !strongSelf.didSetReady { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift index 13b73a9621..6b797d6d23 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift @@ -42,7 +42,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { case stickersMode(PresentationTheme, Bool) case savedGifs(PresentationTheme, Bool) case trendingGifs(PresentationTheme, Bool) - case gifEmotion(Int, PresentationTheme, String, Bool) + case gifEmotion(Int, PresentationTheme, String, TelegramMediaFile?, Bool) var stableId: ChatMediaInputPanelEntryStableId { switch self { @@ -66,7 +66,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .savedGifs case .trendingGifs: return .trendingGifs - case let .gifEmotion(_, _, emoji, _): + case let .gifEmotion(_, _, emoji, _, _): return .gifEmotion(emoji) } } @@ -133,8 +133,15 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return false } - case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsExpanded): - if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded { + case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsFile, lhsExpanded): + if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsFile, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded { + if let lhsFile = lhsFile, let rhsFile = rhsFile { + if !lhsFile.isEqual(to: rhsFile) { + return false + } + } else if (lhsFile != nil) != (rhsFile != nil) { + return false + } return true } else { return false @@ -230,11 +237,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } - case let .gifEmotion(lhsIndex, _, _, _): + case let .gifEmotion(lhsIndex, _, _, _, _): switch rhs { case .stickersMode, .savedGifs, .trendingGifs: return false - case let .gifEmotion(rhsIndex, _, _, _): + case let .gifEmotion(rhsIndex, _, _, _, _): return lhsIndex < rhsIndex default: return true @@ -256,12 +263,12 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { inputNodeInteraction.navigateToCollectionId(collectionId) }) case let .savedStickers(theme, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, expanded: expanded, selected: { + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) case let .recentPacks(theme, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, expanded: expanded, selected: { + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) @@ -284,19 +291,19 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { inputNodeInteraction.navigateToCollectionId(info.id) }) case let .stickersMode(theme, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, expanded: expanded, selected: { + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, expanded: expanded, selected: { inputNodeInteraction.navigateBackToStickers() }) case let .savedGifs(theme, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, expanded: expanded, selected: { + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.recent) }) case let .trendingGifs(theme, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, expanded: expanded, selected: { + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.trending) }) - case let .gifEmotion(_, theme, emoji, expanded): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, expanded: expanded, selected: { + case let .gifEmotion(_, theme, emoji, file, expanded): + return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji, file), theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.emojiSearch(emoji)) }) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index 1b057d98bc..86b384ffc4 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -87,6 +87,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { var inputNodeInteraction: ChatMediaInputNodeInteraction? var currentCollectionId: ItemCollectionId? + private var account: Account? private var currentThumbnailItem: StickerPackThumbnailItem? private var currentExpanded = false private var theme: PresentationTheme? @@ -172,7 +173,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) { self.currentCollectionId = collectionId - + self.account = account var themeUpdated = false if self.theme !== theme { self.theme = theme @@ -262,7 +263,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 4.0, height: expandedBoundingSize.height)) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 6.0), size: titleSize) let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) @@ -314,4 +315,74 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + override func isReorderable(at point: CGPoint) -> Bool { + if self.bounds.contains(point) { + return true + } + return false + } + + override func snapshotForReordering() -> UIView? { + if let account = account, let thumbnailItem = self.currentThumbnailItem { + var imageSize = boundingImageSize + let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false + let containerNode = ASDisplayNode() + let scalingNode = ASDisplayNode() + containerNode.addSubnode(scalingNode) + containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + var snapshotImageNode: TransformImageNode? + var snapshotAnimationNode: AnimatedStickerNode? + switch thumbnailItem { + case let .still(representation): + imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) + + let imageNode = TransformImageNode() + let imageApply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) + imageApply() + imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true)) + scalingNode.addSubnode(imageNode) + + snapshotImageNode = imageNode + case let .animated(resource, _): + let animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers + scalingNode.addSubnode(animatedStickerNode) + + animatedStickerNode.cloneCurrentFrame(from: self.animatedStickerNode) + animatedStickerNode.play(fromIndex: self.animatedStickerNode?.currentFrameIndex) + + snapshotAnimationNode = animatedStickerNode + } + + containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + if let titleView = self.titleNode.view.snapshotContentTree() { + titleView.frame = self.titleNode.frame + scalingNode.view.addSubview(titleView) + } + + let imageFrame = CGRect(origin: CGPoint(x: (expandedBoundingSize.height - imageSize.width) / 2.0, y: (expandedBoundingSize.width - imageSize.height) / 2.0), size: imageSize) + if let imageNode = snapshotImageNode { + imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize) + imageNode.position = imageFrame.center + } + if let animatedStickerNode = snapshotAnimationNode { + animatedStickerNode.frame = imageFrame + animatedStickerNode.updateLayout(size: imageFrame.size) + } + + let expanded = self.currentExpanded + let scale = expanded ? 1.0 : boundingImageScale + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + scalingNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + scalingNode.position = CGPoint(x: boundsSize.width / 2.0 + 3.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0) - 3.0) + + return containerNode.view + } + return nil + } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index af7a0eaf87..499549bcae 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1120,7 +1120,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { audioRecordingInfoContainerNode = ASDisplayNode() self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode - self.insertSubnode(audioRecordingInfoContainerNode, at: 0) + self.clippingNode.insertSubnode(audioRecordingInfoContainerNode, at: 0) } var animateTimeSlideIn = false @@ -1148,7 +1148,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self?.interfaceInteraction?.finishMediaRecording(.dismiss) }) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator - self.insertSubnode(audioRecordingCancelIndicator, at: 0) + self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0) } let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.recordedMediaPreview != nil) @@ -1257,7 +1257,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { audioRecordingDotNode = AnimationNode(animation: "BinRed") self.audioRecordingDotNode = audioRecordingDotNode self.audioRecordingDotNodeDismissed = false - self.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton) + self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton) self.animatingBinNode?.removeFromSupernode() self.animatingBinNode = nil } @@ -1412,7 +1412,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self?.interfaceInteraction?.finishMediaRecording(.send) return true } - self.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) + self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) } self.actionButtons.isAccessibilityElement = false let size: CGFloat = 120.0 @@ -1475,7 +1475,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { contextPlaceholderNode.displaysAsynchronously = false contextPlaceholderNode.isUserInteractionEnabled = false self.contextPlaceholderNode = contextPlaceholderNode - self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) + self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) } let _ = placeholderApply() @@ -1495,7 +1495,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme) self.slowmodePlaceholderNode = slowmodePlaceholderNode - self.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode) + self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode) } let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0)) slowmodePlaceholderNode.updateState(slowmodeState) diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 383a66ef53..08dcbcd4dd 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -71,7 +71,7 @@ private final class PrefetchManagerInnerImpl { } } - let popularEmoji = ["\u{2764}", "👍", "😳", "😒", "🥳"] + let popularEmoji = ["\u{2764}", "👍", "👎", "😳", "😒", "🥳", "😡", "😮", "😂", "😘", "😍", "🙄", "😎"] for emoji in popularEmoji { if let sticker = animatedEmojiStickers[emoji] { if let _ = account.postbox.mediaBox.completedResourcePath(sticker.file.resource) { diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 2e2c8bdc09..d235274ce2 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -221,14 +221,7 @@ final class SharedMediaPlayer { if case .music = playbackData.type { rateValue = 1.0 } else { - switch strongSelf.playbackRate { - case .x1: - rateValue = 1.0 - case .x2: - rateValue = 1.8 - default: - rateValue = 1.0 - } + rateValue = strongSelf.playbackRate.doubleValue } switch playbackData.type {