diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index e9c213c773..7bae52d5e5 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1289,7 +1289,7 @@ public final class ChatListNode: ListView { } } - self.didEndScrolling = { [weak self] in + self.didEndScrolling = { [weak self] _ in guard let strongSelf = self else { return } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index d8a2854c12..63b019afa2 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -1407,7 +1407,7 @@ public final class ContactListNode: ASDisplayNode { } }) - self.listNode.didEndScrolling = { [weak self] in + self.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self { let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) } diff --git a/submodules/ContactListUI/Sources/InviteContactsController.swift b/submodules/ContactListUI/Sources/InviteContactsController.swift index 45bd3d0b8d..7f52577b8d 100644 --- a/submodules/ContactListUI/Sources/InviteContactsController.swift +++ b/submodules/ContactListUI/Sources/InviteContactsController.swift @@ -178,7 +178,7 @@ public class InviteContactsController: ViewController, MFMessageComposeViewContr } } - self.contactsNode.listNode.didEndScrolling = { [weak self] in + self.contactsNode.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { let _ = fixNavigationSearchableListNodeScrolling(strongSelf.contactsNode.listNode, searchNode: searchContentNode) } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index f8615f2ebc..22e53c3068 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -262,6 +262,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var synchronousNodes = false public final var debugInfo = false + public final var useSingleDimensionTouchPoint = false + public var enableExtractedBackgrounds: Bool = false { didSet { if self.enableExtractedBackgrounds != oldValue { @@ -292,8 +294,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public final var beganInteractiveDragging: (CGPoint) -> Void = { _ in } - public final var endedInteractiveDragging: () -> Void = { } - public final var didEndScrolling: (() -> Void)? + public final var endedInteractiveDragging: (CGPoint) -> Void = { _ in } + public final var didEndScrolling: ((Bool) -> Void)? private var currentGeneralScrollDirection: GeneralScrollDirection? public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in } @@ -710,9 +712,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.resetScrollIndicatorFlashTimer(start: true) self.lastContentOffsetTimestamp = 0.0 - self.didEndScrolling?() + self.didEndScrolling?(false) } - self.endedInteractiveDragging() + self.endedInteractiveDragging(self.touchesPosition) } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { @@ -722,7 +724,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.updateHeaderItemsFlashing(animated: true) self.resetScrollIndicatorFlashTimer(start: true) if !scrollView.isTracking { - self.didEndScrolling?() + self.didEndScrolling?(true) } } @@ -3135,16 +3137,38 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture reverseAnimation = reverseBasicAnimation } } - animation.completion = { _ in - for itemNode in temporaryPreviousNodes { - itemNode.removeFromSupernode() - itemNode.extractedBackgroundNode?.removeFromSupernode() - } - for headerNode in temporaryHeaderNodes { - headerNode.removeFromSupernode() + + if scrollToItem.displayLink { + self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, -offset, 0.0) + let offsetAnimation = ListViewAnimation(from: -offset, to: 0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem, beginAt: timestamp, update: { [weak self] progress, currentValue in + if let strongSelf = self { + strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, currentValue, 0.0) + + if progress == 1.0 { + for itemNode in temporaryPreviousNodes { + itemNode.removeFromSupernode() + itemNode.extractedBackgroundNode?.removeFromSupernode() + } + for headerNode in temporaryHeaderNodes { + headerNode.removeFromSupernode() + } + } + } + }) + self.animations.append(offsetAnimation) + } else { + animation.completion = { _ in + for itemNode in temporaryPreviousNodes { + itemNode.removeFromSupernode() + itemNode.extractedBackgroundNode?.removeFromSupernode() + } + for headerNode in temporaryHeaderNodes { + headerNode.removeFromSupernode() + } } + self.layer.add(animation, forKey: nil) } - self.layer.add(animation, forKey: nil) + for itemNode in self.itemNodes { itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -offset), animationCurve: animationCurve, duration: animationDuration) } @@ -3926,7 +3950,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture animation.applyAt(timestamp) if animation.completeAt(timestamp) { - animations.remove(at: i) + self.animations.remove(at: i) animationCount -= 1 i -= 1 } else { @@ -4148,6 +4172,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } public func itemIndexAtPoint(_ point: CGPoint) -> Int? { + var point = point + if self.useSingleDimensionTouchPoint { + point.x = 0.0 + } for itemNode in self.itemNodes { if itemNode.apparentContentFrame.contains(point) { return itemNode.index diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index 56d0e73bec..096f485fc3 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -31,13 +31,15 @@ public struct ListViewScrollToItem { public let animated: Bool public let curve: ListViewAnimationCurve public let directionHint: ListViewScrollToItemDirectionHint + public let displayLink: Bool - public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint) { + public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint, displayLink: Bool = false) { self.index = index self.position = position self.animated = animated self.curve = curve self.directionHint = directionHint + self.displayLink = displayLink } } diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 1edd8073a2..d23236a0ab 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -384,7 +384,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { animation.applyAt(timestamp) if animation.completeAt(timestamp) { - animations.remove(at: i) + self.animations.remove(at: i) animationCount -= 1 i -= 1 } else { diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 1ec92c83e3..9243fb7c4a 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -258,13 +258,11 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture transition.updateFrame(node: navigationBar, frame: CGRect(origin: CGPoint(x: 0.0, y: self.areControlsHidden ? -navigationBarHeight : 0.0), size: CGSize(width: layout.size.width, height: navigationBarHeight))) } - let displayThumbnailPanel = layout.size.width < layout.size.height var thumbnailPanelHeight: CGFloat = 0.0 if let currentThumbnailContainerNode = self.currentThumbnailContainerNode { let panelHeight: CGFloat = 52.0 - if displayThumbnailPanel { - thumbnailPanelHeight = 52.0 - } + thumbnailPanelHeight = panelHeight + let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 40.0 - panelHeight + 4.0 - layout.intrinsicInsets.bottom + (self.areControlsHidden ? 106.0 : 0.0)), size: CGSize(width: layout.size.width, height: panelHeight - 4.0)) transition.updateFrame(node: currentThumbnailContainerNode, frame: thumbnailsFrame) currentThumbnailContainerNode.updateLayout(size: thumbnailsFrame.size, transition: transition) diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 4f14a43e08..8ec0a40ffb 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -70,7 +70,10 @@ public final class GalleryFooterNode: ASDisplayNode { } } - let effectiveThumbnailPanelHeight = self.currentThumbnailPanelHeight ?? thumbnailPanelHeight + var effectiveThumbnailPanelHeight = self.currentThumbnailPanelHeight ?? thumbnailPanelHeight + if layout.size.width > layout.size.height { + effectiveThumbnailPanelHeight = 0.0 + } var backgroundHeight: CGFloat = 0.0 let verticalOffset: CGFloat = isHidden ? (layout.size.width > layout.size.height ? 44.0 : (effectiveThumbnailPanelHeight > 0.0 ? 106.0 : 54.0)) : 0.0 if let footerContentNode = self.currentFooterContentNode { diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 2b090b5ecc..ed023b003b 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -121,7 +121,7 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { public enum StickerPackThumbnailItem: Equatable { case still(TelegramMediaImageRepresentation) - case animated(MediaResource) + case animated(MediaResource, PixelDimensions) public static func ==(lhs: StickerPackThumbnailItem, rhs: StickerPackThumbnailItem) -> Bool { switch lhs { @@ -131,8 +131,8 @@ public enum StickerPackThumbnailItem: Equatable { } else { return false } - case let .animated(lhsResource): - if case let .animated(rhsResource) = rhs, lhsResource.isEqual(to: rhsResource) { + case let .animated(lhsResource, lhsDimensions): + if case let .animated(rhsResource, rhsDimensions) = rhs, lhsResource.isEqual(to: rhsResource), lhsDimensions == rhsDimensions { return true } else { return false @@ -439,7 +439,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { var resourceReference: MediaResourceReference? if let thumbnail = item.packInfo.thumbnail { if item.packInfo.flags.contains(.isAnimated) { - thumbnailItem = .animated(thumbnail.resource) + thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions) resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource) } else { thumbnailItem = .still(thumbnail) @@ -447,7 +447,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } } else if let item = item.topItem { if item.file.isAnimatedSticker { - thumbnailItem = .animated(item.file.resource) + thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) @@ -474,7 +474,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stillImageSize, boundingSize: stillImageSize, intrinsicInsets: UIEdgeInsets())) updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, resource: representation.resource, nilIfEmpty: true) } - case let .animated(resource): + case let .animated(resource, _): imageSize = imageBoundingSize if fileUpdated { @@ -706,10 +706,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let boundingSize = CGSize(width: 34.0, height: 34.0) if let thumbnailItem = thumbnailItem, let imageSize = imageSize { let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: floor((layout.contentSize.height - imageSize.height) / 2.0)), size: imageSize) + var thumbnailDimensions = PixelDimensions(width: 512, height: 512) switch thumbnailItem { - case .still: + case let .still(representation): transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) - case let .animated(resource): + thumbnailDimensions = representation.dimensions + case let .animated(resource, _): transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) let animationNode: AnimatedStickerNode @@ -733,7 +735,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if let placeholderNode = strongSelf.placeholderNode { placeholderNode.frame = imageFrame - placeholderNode.update(backgroundColor: nil, foregroundColor: item.presentationData.theme.list.disclosureArrowColor.blitOver(item.presentationData.theme.list.itemBlocksBackgroundColor, alpha: 0.55), shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.packInfo.immediateThumbnailData, size: imageFrame.size, imageSize: CGSize(width: 100.0, height: 100.0)) + placeholderNode.update(backgroundColor: nil, foregroundColor: item.presentationData.theme.list.disclosureArrowColor.blitOver(item.presentationData.theme.list.itemBlocksBackgroundColor, alpha: 0.55), shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.packInfo.immediateThumbnailData, size: imageFrame.size, imageSize: thumbnailDimensions.cgSize) } } diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 31aa4eda42..47dfeb4141 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -352,7 +352,7 @@ open class ItemListControllerNode: ASDisplayNode { } } - self.listNode.didEndScrolling = { [weak self] in + self.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self { let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h index 5d2fa718d3..91ce61daab 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h @@ -22,6 +22,7 @@ - (void)setZoomedProgress:(CGFloat)progress; +- (void)saveStartImage:(void (^)(void))completion; - (TGCameraPreviewView *)previewView; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h index 80103834ca..2d83e9e92d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h @@ -61,6 +61,7 @@ - (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping allowSelection:(bool)allowSelection allowEditing:(bool)allowEditing document:(bool)document selectionLimit:(int)selectionLimit; +- (void)saveStartImage; - (UIView *)getItemSnapshot:(NSString *)uniqueId; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h index c08deb5ed3..1841ea47ba 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h @@ -77,5 +77,6 @@ typedef enum { + (UIInterfaceOrientation)_interfaceOrientationForDeviceOrientation:(UIDeviceOrientation)orientation; + (UIImage *)startImage; ++ (void)generateStartImageWithImage:(UIImage *)frameImage; @end diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m b/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m index 2c0c95f510..6256244d6d 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m @@ -247,4 +247,15 @@ _iconView.frame = CGRectMake((self.frame.size.width - _iconView.frame.size.width) / 2, (self.frame.size.height - _iconView.frame.size.height) / 2, _iconView.frame.size.width, _iconView.frame.size.height); } +- (void)saveStartImage:(void (^)(void))completion { + [_camera captureNextFrameCompletion:^(UIImage *frameImage) { + [[SQueue concurrentDefaultQueue] dispatch:^{ + [TGCameraController generateStartImageWithImage:frameImage]; + TGDispatchOnMainThread(^{ + completion(); + }); + }]; + }]; +} + @end diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m index 420fe63797..66d38b04d1 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m @@ -107,6 +107,8 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; bool _saveEditedPhotos; TGMenuSheetPallete *_pallete; + + bool _savingStartImage; } @end @@ -347,6 +349,15 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; [_itemsSizeChangedDisposable dispose]; } +- (void)saveStartImage { + _savingStartImage = true; + __weak TGAttachmentCameraView *weakCameraView = _cameraView; + [_cameraView saveStartImage:^{ + __strong TGAttachmentCameraView *strongCameraView = weakCameraView; + [strongCameraView stopPreview]; + }]; +} + - (UIView *)getItemSnapshot:(NSString *)uniqueId { for (UIView *cell in _collectionView.visibleCells) { if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) { @@ -1227,7 +1238,9 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; { [super menuView:menuView didDisappearAnimated:animated]; menuView.tapDismissalAllowed = nil; - [_cameraView stopPreview]; + if (!_savingStartImage) { + [_cameraView stopPreview]; + } } #pragma mark - diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 31b075faef..4ffd7525af 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -182,7 +182,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati carouselItem.stickersContext = paintStickersContext carouselItem.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation) carouselItem.recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + var openedCamera = false + controller.willDismiss = { [weak carouselItem] _ in + if let carouselItem = carouselItem, !openedCamera { + carouselItem.saveStartImage() + } + } carouselItem.cameraPressed = { [weak controller, weak parentController] cameraView in + openedCamera = true if let controller = controller { if let parentController = parentController, parentController.context.currentlyInSplitView() { return diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift index 7bbdc8eda3..fe90b99711 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift @@ -110,7 +110,7 @@ public final class ChannelMembersSearchController: ViewController { } } - self.controllerNode.listNode.didEndScrolling = { [weak self] in + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) } diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index bb6825a4f6..71019a4cc9 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -130,7 +130,7 @@ public class LocalizationListController: ViewController { } } - self.controllerNode.listNode.didEndScrolling = { [weak self] in + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) } diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift index d4c623d5b2..9b00044f63 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift @@ -142,7 +142,7 @@ public class NotificationExceptionsController: ViewController { } } - self.controllerNode.listNode.didEndScrolling = { [weak self] in + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 2f63f71b6f..3a6fa3a677 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -5,10 +5,12 @@ import Display public final class SolidRoundedButtonTheme { public let backgroundColor: UIColor + public let gradientBackgroundColor: UIColor? public let foregroundColor: UIColor - public init(backgroundColor: UIColor, foregroundColor: UIColor) { + public init(backgroundColor: UIColor, gradientBackgroundColor: UIColor? = nil, foregroundColor: UIColor) { self.backgroundColor = backgroundColor + self.gradientBackgroundColor = gradientBackgroundColor self.foregroundColor = foregroundColor } } @@ -59,6 +61,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.title = title self.buttonBackgroundNode = ASDisplayNode() + self.buttonBackgroundNode.clipsToBounds = true self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonBackgroundNode.cornerRadius = cornerRadius if #available(iOS 13.0, *) { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 3499f3dee9..462fedd582 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1382,8 +1382,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else { var outgoingAudioBitrateKbit: Int32? let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 }) - if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 { - outgoingAudioBitrateKbit = value + if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Double { + outgoingAudioBitrateKbit = Int32(value) } genericCallContext = OngoingGroupCallContext(video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index a2578444b8..760f9b2a7f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1119,7 +1119,7 @@ public final class VoiceChatController: ViewController { self.scheduleTextNode.textAlignment = .center self.scheduleTextNode.maximumNumberOfLines = 4 - self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0) + self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0) self.scheduleCancelButton.isHidden = !self.isScheduling self.dateFormatter = DateFormatter() @@ -2410,7 +2410,6 @@ public final class VoiceChatController: ViewController { return [] } - let presentationData = strongSelf.presentationData var items: [ContextMenuItem] = [] if peers.count > 1 { @@ -2582,14 +2581,22 @@ public final class VoiceChatController: ViewController { return } - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title) + let controller = VoiceChatRecordingSetupController(context: strongSelf.context, completion: { [weak self] in + if let strongSelf = self { + strongSelf.call.setShouldBeRecording(true, title: "") strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) strongSelf.call.playTone(.recordingStarted) } }) +// let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in +// if let strongSelf = self, let title = title { +// strongSelf.call.setShouldBeRecording(true, title: title) +// +// strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) +// strongSelf.call.playTone(.recordingStarted) +// } +// }) self?.controller?.present(controller, in: .window(.root)) }))) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift new file mode 100644 index 0000000000..6886200f9f --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift @@ -0,0 +1,597 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import SolidRoundedButtonNode +import PresentationDataUtils + +private let accentColor: UIColor = UIColor(rgb: 0x007aff) + +final class VoiceChatRecordingSetupController: ViewController { + private var controllerNode: VoiceChatRecordingSetupControllerNode { + return self.displayNode as! VoiceChatRecordingSetupControllerNode + } + + private let context: AccountContext + private let completion: () -> Void + + private var animatedIn = false + + private var presentationDataDisposable: Disposable? + + init(context: AccountContext, completion: @escaping () -> Void) { + self.context = context + self.completion = completion + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + + self.blocksBackgroundWhenInOverlay = true + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = VoiceChatRecordingSetupControllerNode(controller: self, context: self.context) + self.controllerNode.completion = { [weak self] in + self?.completion() + } + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + } + + override public func loadView() { + super.loadView() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } +} + +private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + enum MediaMode { + case videoAndAudio + case audioOnly + } + + enum VideoMode { + case portrait + case landscape + } + + private weak var controller: VoiceChatRecordingSetupController? + private let context: AccountContext + private var presentationData: PresentationData + + private let dimNode: ASDisplayNode + private let wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let doneButton: VoiceChatActionButton + private let cancelButton: SolidRoundedButtonNode + private let modeContainerNode: ASDisplayNode + + private let modeSeparatorNode: ASDisplayNode + private let videoAudioButton: HighlightTrackingButtonNode + private let videoAudioTitleNode: ImmediateTextNode + private let videoAudioCheckNode: ASImageNode + + private let audioButton: HighlightTrackingButtonNode + private let audioTitleNode: ImmediateTextNode + private let audioCheckNode: ASImageNode + + private let portraitButton: HighlightTrackingButtonNode + private let portraitIconNode: PreviewIconNode + private let portraitTitleNode: ImmediateTextNode + + private let landscapeButton: HighlightTrackingButtonNode + private let landscapeIconNode: PreviewIconNode + private let landscapeTitleNode: ImmediateTextNode + + private let selectionNode: ASImageNode + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let hapticFeedback = HapticFeedback() + + private let readyDisposable = MetaDisposable() + + private var mediaMode: MediaMode = .videoAndAudio + private var videoMode: VideoMode = .portrait + + var completion: (() -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + init(controller: VoiceChatRecordingSetupController, context: AccountContext) { + self.controller = controller + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 + + let backgroundColor = UIColor(rgb: 0x1c1c1e) + let textColor: UIColor = .white + let buttonColor: UIColor = UIColor(rgb: 0x2b2b2f) + let buttonTextColor: UIColor = .white + let blurStyle: UIBlurEffect.Style = .dark + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + }) + + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = backgroundColor + + let title = "Record Voice Chat" + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor) + + self.doneButton = VoiceChatActionButton() + + self.cancelButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false) + self.cancelButton.title = self.presentationData.strings.Common_Cancel + + self.modeContainerNode = ASDisplayNode() + self.modeContainerNode.clipsToBounds = true + self.modeContainerNode.cornerRadius = 11.0 + self.modeContainerNode.backgroundColor = UIColor(rgb: 0x303032) + + self.modeSeparatorNode = ASDisplayNode() + self.modeSeparatorNode.backgroundColor = UIColor(rgb: 0x404041) + + self.videoAudioButton = HighlightTrackingButtonNode() + self.videoAudioTitleNode = ImmediateTextNode() + self.videoAudioTitleNode.attributedText = NSAttributedString(string: "Video and Audio", font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) + self.videoAudioCheckNode = ASImageNode() + self.videoAudioCheckNode.displaysAsynchronously = false + self.videoAudioCheckNode.image = UIImage(bundleImageName: "Call/Check") + + self.audioButton = HighlightTrackingButtonNode() + self.audioTitleNode = ImmediateTextNode() + self.audioTitleNode.attributedText = NSAttributedString(string: "Only Audio", font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) + self.audioCheckNode = ASImageNode() + self.audioCheckNode.displaysAsynchronously = false + self.audioCheckNode.image = UIImage(bundleImageName: "Call/Check") + + self.portraitButton = HighlightTrackingButtonNode() + self.portraitButton.backgroundColor = UIColor(rgb: 0x303032) + self.portraitButton.cornerRadius = 11.0 + self.portraitIconNode = PreviewIconNode() + self.portraitTitleNode = ImmediateTextNode() + self.portraitTitleNode.attributedText = NSAttributedString(string: "Portrait", font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + + self.landscapeButton = HighlightTrackingButtonNode() + self.landscapeButton.backgroundColor = UIColor(rgb: 0x303032) + self.landscapeButton.cornerRadius = 11.0 + self.landscapeIconNode = PreviewIconNode() + self.landscapeTitleNode = ImmediateTextNode() + self.landscapeTitleNode.attributedText = NSAttributedString(string: "Landscape", font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + + self.selectionNode = ASImageNode() + self.selectionNode.displaysAsynchronously = false + self.selectionNode.image = generateImage(CGSize(width: 174.0, height: 140.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth: CGFloat = 2.0 + + let path = UIBezierPath(roundedRect: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerRadius: 11.0) + let cgPath = path.cgPath.copy(strokingWithWidth: lineWidth, lineCap: .round, lineJoin: .round, miterLimit: 10.0) + context.addPath(cgPath) + context.clip() + + let colors: [CGColor] = [UIColor(rgb: 0x5064fd).cgColor, UIColor(rgb: 0xe76598).cgColor] + var locations: [CGFloat] = [0.0, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + }) + self.selectionNode.isUserInteractionEnabled = false + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.backgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.doneButton) + self.contentContainerNode.addSubnode(self.cancelButton) + self.contentContainerNode.addSubnode(self.modeContainerNode) + + self.contentContainerNode.addSubnode(self.videoAudioTitleNode) + self.contentContainerNode.addSubnode(self.videoAudioCheckNode) + self.contentContainerNode.addSubnode(self.videoAudioButton) + + self.contentContainerNode.addSubnode(self.modeSeparatorNode) + + self.contentContainerNode.addSubnode(self.audioTitleNode) + self.contentContainerNode.addSubnode(self.audioCheckNode) + self.contentContainerNode.addSubnode(self.audioButton) + + self.contentContainerNode.addSubnode(self.portraitButton) + self.contentContainerNode.addSubnode(self.portraitIconNode) + self.contentContainerNode.addSubnode(self.portraitTitleNode) + + self.contentContainerNode.addSubnode(self.landscapeButton) + self.contentContainerNode.addSubnode(self.landscapeIconNode) + self.contentContainerNode.addSubnode(self.landscapeTitleNode) + + self.contentContainerNode.addSubnode(self.selectionNode) + + self.videoAudioButton.addTarget(self, action: #selector(self.videoAudioPressed), forControlEvents: .touchUpInside) + self.audioButton.addTarget(self, action: #selector(self.audioPressed), forControlEvents: .touchUpInside) + + self.portraitButton.addTarget(self, action: #selector(self.portraitPressed), forControlEvents: .touchUpInside) + self.landscapeButton.addTarget(self, action: #selector(self.landscapePressed), forControlEvents: .touchUpInside) + + self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) + + self.cancelButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.cancel?() + } + } + } + + @objc private func donePressed() { + self.completion?() + self.dismiss?() + } + + @objc private func videoAudioPressed() { + self.mediaMode = .videoAndAudio + + if let (layout, navigationHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + @objc private func audioPressed() { + self.mediaMode = .audioOnly + + if let (layout, navigationHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + @objc private func portraitPressed() { + self.mediaMode = .videoAndAudio + self.videoMode = .portrait + + if let (layout, navigationHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + @objc private func landscapePressed() { + self.mediaMode = .videoAndAudio + self.videoMode = .landscape + + if let (layout, navigationHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancel?() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + let isLandscape: Bool + if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { + isLandscape = true + } else { + isLandscape = false + } + + + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + let buttonOffset: CGFloat = 60.0 + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let titleHeight: CGFloat = 54.0 + var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + let innerContentHeight: CGFloat = 287.0 + var width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + if isLandscape { + contentHeight = layout.size.height + width = layout.size.width + } else { + contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + innerContentHeight + buttonOffset + } + + let inset: CGFloat = 16.0 + let sideInset = floor((layout.size.width - width) / 2.0) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentFrame = contentContainerFrame + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + transition.updateAlpha(node: self.titleNode, alpha: isLandscape ? 0.0 : 1.0) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let itemHeight: CGFloat = 44.0 + + transition.updateFrame(node: self.modeContainerNode, frame: CGRect(x: inset, y: 56.0, width: contentFrame.width - inset * 2.0, height: itemHeight * 2.0)) + + transition.updateFrame(node: self.videoAudioButton, frame: CGRect(x: inset, y: 56.0, width: contentFrame.width - inset * 2.0, height: itemHeight)) + transition.updateFrame(node: self.videoAudioCheckNode, frame: CGRect(x: contentFrame.width - inset - 16.0 - 20.0, y: 56.0 + floorToScreenPixels((itemHeight - 16.0) / 2.0), width: 16.0, height: 16.0)) + self.videoAudioCheckNode.isHidden = self.mediaMode != .videoAndAudio + + let videoAudioSize = self.videoAudioTitleNode.updateLayout(CGSize(width: contentFrame.width - inset * 2.0, height: itemHeight)) + transition.updateFrame(node: self.videoAudioTitleNode, frame: CGRect(x: inset + 16.0, y: 56.0 + floorToScreenPixels((itemHeight - videoAudioSize.height) / 2.0), width: videoAudioSize.width, height: videoAudioSize.height)) + + transition.updateFrame(node: self.audioButton, frame: CGRect(x: inset, y: 56.0 + itemHeight, width: contentFrame.width - inset * 2.0, height: itemHeight)) + transition.updateFrame(node: self.audioCheckNode, frame: CGRect(x: contentFrame.width - inset - 16.0 - 20.0, y: 56.0 + itemHeight + floorToScreenPixels((itemHeight - 16.0) / 2.0), width: 16.0, height: 16.0)) + self.audioCheckNode.isHidden = self.mediaMode != .audioOnly + + let audioSize = self.audioTitleNode.updateLayout(CGSize(width: contentFrame.width - inset * 2.0, height: itemHeight)) + transition.updateFrame(node: self.audioTitleNode, frame: CGRect(x: inset + 16.0, y: 56.0 + itemHeight + floorToScreenPixels((itemHeight - audioSize.height) / 2.0), width: audioSize.width, height: audioSize.height)) + + transition.updateFrame(node: self.modeSeparatorNode, frame: CGRect(x: inset + 16.0, y: 56.0 + itemHeight, width: contentFrame.width - inset * 2.0 - 16.0, height: UIScreenPixel)) + + var buttonsAlpha: CGFloat = 1.0 + if case .audioOnly = self.mediaMode { + buttonsAlpha = 0.3 + } + + transition.updateAlpha(node: self.portraitButton, alpha: buttonsAlpha) + transition.updateAlpha(node: self.portraitIconNode, alpha: buttonsAlpha) + transition.updateAlpha(node: self.portraitTitleNode, alpha: buttonsAlpha) + + transition.updateAlpha(node: self.landscapeButton, alpha: buttonsAlpha) + transition.updateAlpha(node: self.landscapeIconNode, alpha: buttonsAlpha) + transition.updateAlpha(node: self.landscapeTitleNode, alpha: buttonsAlpha) + + transition.updateAlpha(node: self.selectionNode, alpha: buttonsAlpha) + + self.portraitTitleNode.attributedText = NSAttributedString(string: "Portrait", font: Font.semibold(15.0), textColor: self.videoMode == .portrait ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + self.landscapeTitleNode.attributedText = NSAttributedString(string: "Landscape", font: Font.semibold(15.0), textColor: self.videoMode == .landscape ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + + let buttonWidth = floorToScreenPixels((contentFrame.width - inset * 2.0 - 11.0) / 2.0) + let portraitButtonFrame = CGRect(x: inset, y: 56.0 + itemHeight * 2.0 + 25.0, width: buttonWidth, height: 140.0) + transition.updateFrame(node: self.portraitButton, frame: portraitButtonFrame) + transition.updateFrame(node: self.portraitIconNode, frame: CGRect(x: portraitButtonFrame.minX + floorToScreenPixels((portraitButtonFrame.width - 72.0) / 2.0), y: portraitButtonFrame.minY + floorToScreenPixels((portraitButtonFrame.height - 122.0) / 2.0), width: 76.0, height: 122.0)) + self.portraitIconNode.updateLayout(landscape: false) + let portraitSize = self.portraitTitleNode.updateLayout(CGSize(width: buttonWidth, height: 30.0)) + transition.updateFrame(node: self.portraitTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(portraitButtonFrame.center.x - portraitSize.width / 2.0), y: portraitButtonFrame.maxY + 7.0), size: portraitSize)) + + let landscapeButtonFrame = CGRect(x: portraitButtonFrame.maxX + 11.0, y: portraitButtonFrame.minY, width: portraitButtonFrame.width, height: portraitButtonFrame.height) + transition.updateFrame(node: self.landscapeButton, frame: landscapeButtonFrame) + transition.updateFrame(node: self.landscapeIconNode, frame: CGRect(x: landscapeButtonFrame.minX + floorToScreenPixels((landscapeButtonFrame.width - 122.0) / 2.0), y: landscapeButtonFrame.minY + floorToScreenPixels((landscapeButtonFrame.height - 76.0) / 2.0), width: 122.0, height: 76.0)) + self.landscapeIconNode.updateLayout(landscape: true) + let landscapeSize = self.landscapeTitleNode.updateLayout(CGSize(width: buttonWidth, height: 30.0)) + transition.updateFrame(node: self.landscapeTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(landscapeButtonFrame.center.x - landscapeSize.width / 2.0), y: landscapeButtonFrame.maxY + 7.0), size: landscapeSize)) + + let centralButtonSide = min(contentFrame.width, layout.size.height) - 32.0 + let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) + + let buttonInset: CGFloat = 16.0 + let doneButtonPreFrame = CGRect(x: buttonInset, y: contentHeight - 50.0 - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width - buttonInset * 2.0, height: 50.0) + let doneButtonFrame = CGRect(origin: CGPoint(x: floor(doneButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(doneButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + transition.updateFrame(node: self.doneButton, frame: doneButtonFrame) + + if self.videoMode == .portrait { + self.selectionNode.frame = portraitButtonFrame.insetBy(dx: -1.0, dy: -1.0) + } else { + self.selectionNode.frame = landscapeButtonFrame.insetBy(dx: -1.0, dy: -1.0) + } + + self.doneButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: .button(text: "Start Recording"), title: "", subtitle: "", dark: false, small: false) + + let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight)) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + } +} + +private class PreviewIconNode: ASDisplayNode { + private let avatar1Node: ASImageNode + private let avatar2Node: ASImageNode + private let avatar3Node: ASImageNode + private let avatar4Node: ASImageNode + + override init() { + self.avatar1Node = ASImageNode() + self.avatar1Node.cornerRadius = 4.0 + self.avatar1Node.displaysAsynchronously = false + self.avatar1Node.backgroundColor = UIColor(rgb: 0x834fff) + + self.avatar2Node = ASImageNode() + self.avatar2Node.cornerRadius = 4.0 + self.avatar2Node.displaysAsynchronously = false + self.avatar2Node.backgroundColor = UIColor(rgb: 0x63d5c9) + + self.avatar3Node = ASImageNode() + self.avatar3Node.cornerRadius = 4.0 + self.avatar3Node.displaysAsynchronously = false + self.avatar3Node.backgroundColor = UIColor(rgb: 0xccff60) + + self.avatar4Node = ASImageNode() + self.avatar4Node.cornerRadius = 4.0 + self.avatar4Node.displaysAsynchronously = false + self.avatar4Node.backgroundColor = UIColor(rgb: 0xf5512a) + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.avatar1Node) + self.addSubnode(self.avatar2Node) + self.addSubnode(self.avatar3Node) + self.addSubnode(self.avatar4Node) + } + + func updateLayout(landscape: Bool) { + if landscape { + self.avatar1Node.frame = CGRect(x: 0.0, y: 0.0, width: 96.0, height: 76.0) + self.avatar2Node.frame = CGRect(x: 98.0, y: 0.0, width: 24.0, height: 24.0) + self.avatar3Node.frame = CGRect(x: 98.0, y: 26.0, width: 24.0, height: 24.0) + self.avatar4Node.frame = CGRect(x: 98.0, y: 52.0, width: 24.0, height: 24.0) + } else { + self.avatar1Node.frame = CGRect(x: 0.0, y: 0.0, width: 76.0, height: 96.0) + self.avatar2Node.frame = CGRect(x: 0.0, y: 98.0, width: 24.0, height: 24.0) + self.avatar3Node.frame = CGRect(x: 26.0, y: 98.0, width: 24.0, height: 24.0) + self.avatar4Node.frame = CGRect(x: 52.0, y: 98.0, width: 24.0, height: 24.0) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index 0eed25d054..34a3670355 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -3,7 +3,6 @@ import Postbox import TelegramApi import SwiftSignalKit - private struct SearchStickersConfiguration { static var defaultValue: SearchStickersConfiguration { return SearchStickersConfiguration(cacheTimeout: 86400) @@ -16,8 +15,8 @@ private struct SearchStickersConfiguration { } static func with(appConfiguration: AppConfiguration) -> SearchStickersConfiguration { - if let data = appConfiguration.data, let value = data["stickers_emoji_cache_time"] as? Int32 { - return SearchStickersConfiguration(cacheTimeout: value) + if let data = appConfiguration.data, let value = data["stickers_emoji_cache_time"] as? Double { + return SearchStickersConfiguration(cacheTimeout: Int32(value)) } else { return .defaultValue } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 38957258e6..af36a3f519 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -103,6 +103,33 @@ public final class ApplicationSpecificTimestampNotice: NoticeEntry { } } +public final class ApplicationSpecificInt64ArrayNotice: NoticeEntry { + public let values: [Int64] + + public init(values: [Int64]) { + self.values = values + } + + public init(decoder: PostboxDecoder) { + self.values = decoder.decodeInt64ArrayForKey("v") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64Array(self.values, forKey: "v") + } + + public func isEqual(to: NoticeEntry) -> Bool { + if let to = to as? ApplicationSpecificInt64ArrayNotice { + if self.values != to.values { + return false + } + return true + } else { + return false + } + } +} + private func noticeNamespace(namespace: Int32) -> ValueBoxKey { let key = ValueBoxKey(length: 4) key.setInt32(0, value: namespace) @@ -138,6 +165,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case chatFolderTips = 19 case locationProximityAlertTip = 20 case nextChatSuggestionTip = 21 + case dismissedTrendingStickerPacks = 22 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -273,6 +301,10 @@ private struct ApplicationSpecificNoticeKeys { static func nextChatSuggestionTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.nextChatSuggestionTip.key) } + + static func dismissedTrendingStickerPacks() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedTrendingStickerPacks.key) + } } public struct ApplicationSpecificNotice { @@ -763,6 +795,23 @@ public struct ApplicationSpecificNotice { } } + public static func dismissedTrendingStickerPacks(accountManager: AccountManager) -> Signal<[Int64]?, NoError> { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks()) + |> map { view -> [Int64]? in + if let value = view.value as? ApplicationSpecificInt64ArrayNotice { + return value.values + } else { + return nil + } + } + } + + public static func setDismissedTrendingStickerPacks(accountManager: AccountManager, values: [Int64]) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks(), ApplicationSpecificInt64ArrayNotice(values: values)) + } + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/Contents.json new file mode 100644 index 0000000000..ed9cc18e76 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RecordCheck.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/RecordCheck.png b/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/RecordCheck.png new file mode 100644 index 0000000000..913252c8f9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Check.imageset/RecordCheck.png differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c2245fb985..9d4d085cfe 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1103,8 +1103,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return interfaceState }.updatedInputMode { current in - if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil) + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) } return current } @@ -1123,8 +1123,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in var current = current current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil) + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) } return current } @@ -1167,8 +1167,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in - if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil) + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) } return current } @@ -7195,7 +7195,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } - if case let .media(_, expanded) = strongSelf.presentationInterfaceState.inputMode, expanded != nil { + if case let .media(_, expanded, _) = strongSelf.presentationInterfaceState.inputMode, expanded != nil { return false } @@ -10033,8 +10033,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return interfaceState } state = state.updatedInputMode { current in - if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil) + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) } return current } @@ -12477,7 +12477,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(interfaceState.effectiveInputState) - }.updatedInputMode({ _ in ChatInputMode.media(mode: .other, expanded: nil) }) + }.updatedInputMode({ _ in ChatInputMode.media(mode: .other, expanded: nil, focused: false) }) }) } }), diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 9d03bb70b9..878ac42dd9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -820,7 +820,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { insets = layout.insets(options: [.input]) } - + if case .overlay = self.chatPresentationInterfaceState.mode { insets.top = 44.0 } else { @@ -1152,7 +1152,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var displayTopDimNode = false let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil var expandTopDimNode = false - if case let .media(_, expanded) = self.chatPresentationInterfaceState.inputMode, expanded != nil { + if case let .media(_, expanded, _) = self.chatPresentationInterfaceState.inputMode, expanded != nil { displayTopDimNode = true expandTopDimNode = true } @@ -1230,7 +1230,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let apparentSecondaryInputPanelFrame = secondaryInputPanelFrame var apparentInputBackgroundFrame = inputBackgroundFrame var apparentNavigateButtonsFrame = navigateButtonsFrame - if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, let expanded = maybeExpanded, case .search = expanded, let inputPanelFrame = inputPanelFrame { + if case let .media(_, maybeExpanded, _) = self.chatPresentationInterfaceState.inputMode, let expanded = maybeExpanded, case .search = expanded, let inputPanelFrame = inputPanelFrame { let verticalOffset = -inputPanelFrame.height - 41.0 apparentInputPanelFrame = inputPanelFrame.offsetBy(dx: 0.0, dy: verticalOffset) apparentInputBackgroundFrame.size.height -= verticalOffset @@ -1854,11 +1854,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, chatLocation: self.chatPresentationInterfaceState.chatLocation, controllerInteraction: self.controllerInteraction, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, theme: theme, strings: strings, fontSize: fontSize, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(_, expanded) = state.inputMode { + if case let .media(_, expanded, focused) = state.inputMode { if value { - return (.media(mode: .gif, expanded: expanded), nil) + return (.media(mode: .gif, expanded: expanded, focused: focused), nil) } else { - return (.media(mode: .other, expanded: expanded), nil) + return (.media(mode: .other, expanded: expanded, focused: focused), nil) } } else { return (state.inputMode, nil) @@ -2112,8 +2112,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { @objc func topDimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(mode, expanded) = state.inputMode, expanded != nil { - return (.media(mode: mode, expanded: nil), nil) + if case let .media(mode, expanded, focused) = state.inputMode, expanded != nil { + return (.media(mode: mode, expanded: nil, focused: focused), nil) } else { return (state.inputMode, nil) } @@ -2122,10 +2122,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func scrollToTop() { - if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, maybeExpanded != nil { + if case let .media(_, maybeExpanded, _) = self.chatPresentationInterfaceState.inputMode, maybeExpanded != nil { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(mode, expanded) = state.inputMode, expanded != nil { - return (.media(mode: mode, expanded: expanded), nil) + if case let .media(mode, expanded, focused) = state.inputMode, expanded != nil { + return (.media(mode: mode, expanded: expanded, focused: focused), nil) } else { return (state.inputMode, nil) } @@ -2259,7 +2259,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { |> deliverOnMainQueue).start(next: { [weak self] in self?.openStickersDisposable = nil self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(mode: .other, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) }) } diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index bce351272f..b4c4d099d4 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -175,6 +175,10 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke }, openSettings: { }, + openTrending: { _ in + }, + dismissTrendingPacks: { _ in + }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { @@ -342,6 +346,10 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC }, openSettings: { }, + openTrending: { _ in + }, + dismissTrendingPacks: { _ in + }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3cfa3dea3c..0c0f4a179f 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1167,7 +1167,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self?.beganDragging?() } - self.endedInteractiveDragging = { [weak self] in + self.endedInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } @@ -1177,7 +1177,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - self.didEndScrolling = { [weak self] in + self.didEndScrolling = { [weak self] _ in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 7fd52addda..31eeb06242 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -311,9 +311,6 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte stickersEnabled = false } } -// if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo { -// accessoryItems.append(.commands) -// } else if chatPresentationInterfaceState.hasBots { accessoryItems.append(.commands) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 4195ba9e65..0960ba0fb8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -23,11 +23,11 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: let inputNode = ChatMediaInputNode(context: context, peerId: peerId, chatLocation: chatPresentationInterfaceState.chatLocation, controllerInteraction: controllerInteraction, chatWallpaper: chatPresentationInterfaceState.chatWallpaper, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in if let interfaceInteraction = interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(_, expanded) = state.inputMode { + if case let .media(_, expanded, focused) = state.inputMode { if value { - return (.media(mode: .gif, expanded: expanded), nil) + return (.media(mode: .gif, expanded: expanded, focused: focused), nil) } else { - return (.media(mode: .other, expanded: expanded), nil) + return (.media(mode: .other, expanded: expanded, focused: focused), nil) } } else { return (state.inputMode, nil) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift index 8b51713f92..2e1671d13e 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift @@ -8,6 +8,7 @@ import MergeLists enum ChatMediaInputGridEntryStableId: Equatable, Hashable { case search + case trendingList case peerSpecificSetup case sticker(ItemCollectionId, ItemCollectionItemIndex.Id) case trending(ItemCollectionId) @@ -15,6 +16,7 @@ enum ChatMediaInputGridEntryStableId: Equatable, Hashable { enum ChatMediaInputGridEntryIndex: Equatable, Comparable { case search + case trendingList case peerSpecificSetup(dismissed: Bool) case collectionIndex(ItemCollectionViewEntryIndex) case trending(ItemCollectionId, Int) @@ -23,6 +25,8 @@ enum ChatMediaInputGridEntryIndex: Equatable, Comparable { switch self { case .search: return .search + case .trendingList: + return .trendingList case .peerSpecificSetup: return .peerSpecificSetup case let .collectionIndex(index): @@ -40,9 +44,16 @@ enum ChatMediaInputGridEntryIndex: Equatable, Comparable { } else { return true } + case .trendingList: + switch rhs { + case .search, .trendingList: + return false + case .peerSpecificSetup, .collectionIndex, .trending: + return true + } case let .peerSpecificSetup(lhsDismissed): switch rhs { - case .search, .peerSpecificSetup: + case .search, .trendingList, .peerSpecificSetup: return false case let .collectionIndex(index): if lhsDismissed { @@ -59,7 +70,7 @@ enum ChatMediaInputGridEntryIndex: Equatable, Comparable { } case let .collectionIndex(lhsIndex): switch rhs { - case .search: + case .search, .trendingList: return false case let .peerSpecificSetup(dismissed): if dismissed { @@ -74,7 +85,7 @@ enum ChatMediaInputGridEntryIndex: Equatable, Comparable { } case let .trending(_, lhsIndex): switch rhs { - case .search, .peerSpecificSetup, .collectionIndex: + case .search, .trendingList, .peerSpecificSetup, .collectionIndex: return false case let .trending(_, rhsIndex): return lhsIndex < rhsIndex @@ -85,6 +96,7 @@ enum ChatMediaInputGridEntryIndex: Equatable, Comparable { enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { case search(theme: PresentationTheme, strings: PresentationStrings) + case trendingList(theme: PresentationTheme, strings: PresentationStrings, packs: [FeaturedStickerPackItem]) case peerSpecificSetup(theme: PresentationTheme, strings: PresentationStrings, dismissed: Bool) case sticker(index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, stickerPackInfo: StickerPackCollectionInfo?, canManagePeerSpecificPack: Bool?, maybeManageable: Bool, theme: PresentationTheme) case trending(TrendingPanePackEntry) @@ -93,6 +105,8 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { switch self { case .search: return .search + case .trendingList: + return .trendingList case let .peerSpecificSetup(_, _, dismissed): return .peerSpecificSetup(dismissed: dismissed) case let .sticker(index, _, _, _, _, _): @@ -120,6 +134,26 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { } else { return false } + case let .trendingList(lhsTheme, lhsStrings, lhsPacks): + if case let .trendingList(rhsTheme, rhsStrings, rhsPacks) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + for i in 0 ..< lhsPacks.count { + if lhsPacks[i].unread != rhsPacks[i].unread { + return false + } + if lhsPacks[i].info != rhsPacks[i].info { + return false + } + } + return true + } else { + return false + } case let .peerSpecificSetup(lhsTheme, lhsStrings, lhsDismissed): if case let .peerSpecificSetup(rhsTheme, rhsStrings, rhsDismissed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDismissed == rhsDismissed { return true @@ -169,6 +203,10 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { inputNodeInteraction.toggleSearch(true, .sticker, "") }) + case let .trendingList(theme, strings, packs): + return StickerPaneTrendingListGridItem(account: account, theme: theme, strings: strings, trendingPacks: packs, inputNodeInteraction: inputNodeInteraction, dismiss: { + inputNodeInteraction.dismissTrendingPacks(packs.map { $0.info.id }) + }) case let .peerSpecificSetup(theme, strings, dismissed): return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: { inputNodeInteraction.openPeerSpecificSettings() diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index cd4d66d743..aabb9398f6 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -222,16 +222,18 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) + self.currentExpanded = expanded 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)) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index c7e4cf9cd5..0500b8ced2 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -7,6 +7,7 @@ import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences +import TelegramNotices import MergeLists import AccountContext import StickerPackPreviewUI @@ -67,7 +68,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle switch toEntries[i] { case .search, .peerSpecificSetup, .trending: break - case .sticker: + case .trendingList, .sticker: scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .immediate, directionHint: .down, adjustForSection: true, adjustForTopInset: true) } } @@ -140,7 +141,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle var firstIndexInSectionOffset = 0 if !toEntries.isEmpty { switch toEntries[0].index { - case .search, .peerSpecificSetup, .trending: + case .search, .trendingList, .peerSpecificSetup, .trending: break case let .collectionIndex(index): firstIndexInSectionOffset = Int(index.itemIndex.index) @@ -152,14 +153,11 @@ 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, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, 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)) } - if let hasUnreadTrending = hasUnreadTrending { - entries.append(.trending(hasUnreadTrending, theme, expanded)) - } if let savedStickers = savedStickers, !savedStickers.items.isEmpty { entries.append(.savedStickers(theme, expanded)) } @@ -221,7 +219,7 @@ func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [Str return entries } -func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasSearch: Bool = true, hasAccessories: Bool = true, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { +func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, trendingPacks: [FeaturedStickerPackItem], dismissedTrendingStickerPacks: [ItemCollectionId.Id]? = nil, hasSearch: Bool = true, hasAccessories: Bool = true, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] if hasSearch && view.lower == nil { @@ -249,6 +247,14 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered } } + var trendingIsDismissed = false + if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, trendingPacks.map({ $0.info.id.id }) == dismissedTrendingStickerPacks { + trendingIsDismissed = true + } + if !trendingIsDismissed { + entries.append(.trendingList(theme: theme, strings: strings, packs: trendingPacks)) + } + if let recentStickers = recentStickers, !recentStickers.items.isEmpty { let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0) var addedCount = 0 @@ -346,6 +352,8 @@ final class ChatMediaInputNodeInteraction { let navigateBackToStickers: () -> Void let setGifMode: (ChatMediaInputGifMode) -> Void let openSettings: () -> Void + let openTrending: (ItemCollectionId?) -> Void + let dismissTrendingPacks: ([ItemCollectionId]) -> Void let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void let openPeerSpecificSettings: () -> Void let dismissPeerSpecificSettings: () -> Void @@ -360,11 +368,13 @@ final class ChatMediaInputNodeInteraction { var displayStickerPlaceholder = true var displayStickerPackManageControls = true - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId self.navigateBackToStickers = navigateBackToStickers self.setGifMode = setGifMode self.openSettings = openSettings + self.openTrending = openTrending + self.dismissTrendingPacks = dismissTrendingPacks self.toggleSearch = toggleSearch self.openPeerSpecificSettings = openPeerSpecificSettings self.dismissPeerSpecificSettings = dismissPeerSpecificSettings @@ -451,14 +461,15 @@ final class ChatMediaInputNode: ChatInputNode { private var currentView: ItemCollectionsView? private let dismissedPeerSpecificStickerPack = Promise() - private var panelCollapseScrollToIndex: Int? - private let panelExpandedPromise = ValuePromise(false) - private var panelExpanded: Bool = false { + private var panelFocusScrollToIndex: Int? + private var panelFocusInitialPosition: CGPoint? + private let panelIsFocusedPromise = ValuePromise(false) + private var panelIsFocused: Bool = false { didSet { - self.panelExpandedPromise.set(self.panelExpanded) + self.panelIsFocusedPromise.set(self.panelIsFocused) } } - private var panelCollapseTimer: SwiftSignalKit.Timer? + private var panelFocusTimer: SwiftSignalKit.Timer? var requestDisableStickerAnimations: ((Bool) -> Void)? @@ -494,17 +505,15 @@ final class ChatMediaInputNode: ChatInputNode { self.themeAndStringsPromise = Promise((theme, strings)) self.collectionListPanel = ASDisplayNode() - self.collectionListPanel.clipsToBounds = true self.collectionListSeparator = ASDisplayNode() self.collectionListSeparator.isLayerBacked = true self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor self.collectionListContainer = CollectionListContainerNode() - self.collectionListContainer.clipsToBounds = true self.listView = ListView() -// self.listView.clipsToBounds = false + self.listView.useSingleDimensionTouchPoint = true 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 @@ -512,7 +521,7 @@ final class ChatMediaInputNode: ChatInputNode { } self.gifListView = ListView() -// self.gifListView.clipsToBounds = false + self.gifListView.useSingleDimensionTouchPoint = true self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.gifListView.accessibilityPageScrolledString = { row, count in @@ -550,6 +559,7 @@ final class ChatMediaInputNode: ChatInputNode { } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( context: strongSelf.context, + highlightedPackId: nil, sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { @@ -609,6 +619,23 @@ final class ChatMediaInputNode: ChatInputNode { controller.navigationPresentation = .modal strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) } + }, openTrending: { [weak self] packId in + if let strongSelf = self { + strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( + context: strongSelf.context, + highlightedPackId: packId, + sendSticker: { + fileReference, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + } else { + return false + } + } + )) + } + }, dismissTrendingPacks: { packIds in + let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: packIds.map { $0.id }).start() }, toggleSearch: { [weak self] value, searchMode, query in if let strongSelf = self { if let searchMode = searchMode, value { @@ -636,8 +663,8 @@ final class ChatMediaInputNode: ChatInputNode { if let strongSelf = self { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: .search(searchMode)) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: .search(searchMode), focused: focused) default: return current } @@ -648,8 +675,8 @@ final class ChatMediaInputNode: ChatInputNode { } else { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: nil) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: nil, focused: focused) default: return current } @@ -860,8 +887,8 @@ final class ChatMediaInputNode: ChatInputNode { let previousView = Atomic(value: nil) let transitionQueue = Queue() - let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelExpandedPromise.get()) - |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded -> (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)) + |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate @@ -884,21 +911,10 @@ final class ChatMediaInputNode: ChatInputNode { for info in view.collectionInfos { installedPacks.insert(info.0) } - - var hasUnreadTrending: Bool? - for pack in trendingPacks { - if hasUnreadTrending == nil { - hasUnreadTrending = false - } - if pack.unread { - hasUnreadTrending = true - break - } - } - - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme, expanded: panelExpanded) + + 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) - var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) + 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 { var hasTopSeparator = true @@ -943,7 +959,9 @@ final class ChatMediaInputNode: ChatInputNode { if let topVisibleSection = visibleItems.topSectionVisible as? ChatMediaInputStickerGridSection { topVisibleCollectionId = topVisibleSection.collectionId } else if let topVisible = visibleItems.topVisible { - if let item = topVisible.1 as? ChatMediaInputStickerGridItem { + if let _ = topVisible.1 as? StickerPaneTrendingListGridItem { + topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) + } else if let item = topVisible.1 as? ChatMediaInputStickerGridItem { topVisibleCollectionId = item.index.collectionId } else if let _ = topVisible.1 as? StickerPanePeerSpecificSetupGridItem { topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) @@ -998,32 +1016,79 @@ final class ChatMediaInputNode: ChatInputNode { } self.listView.beganInteractiveDragging = { [weak self] position in - if let strongSelf = self, false { - if !strongSelf.panelExpanded, let index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { - strongSelf.panelCollapseScrollToIndex = index + if let strongSelf = self { + strongSelf.panelFocusTimer?.invalidate() + var position = position + var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) + if index == nil { + position.y += 10.0 + index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) + } + if let index = index { + strongSelf.panelFocusScrollToIndex = index + strongSelf.panelFocusInitialPosition = position + } + strongSelf.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: true)) + } else { + return (inputTextState, inputMode) + } } - strongSelf.updateIsExpanded(true) } } - self.listView.didEndScrolling = { [weak self] in - if let strongSelf = self, false { - strongSelf.setupCollapseTimer() + self.listView.endedInteractiveDragging = { [weak self] position in + if let strongSelf = self { + strongSelf.panelFocusInitialPosition = position + } + } + + self.listView.didEndScrolling = { [weak self] decelerated in + if let strongSelf = self { + if decelerated { + strongSelf.panelFocusScrollToIndex = nil + strongSelf.panelFocusInitialPosition = nil + } + strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5) } } self.gifListView.beganInteractiveDragging = { [weak self] position in - if let strongSelf = self, false { - if !strongSelf.panelExpanded, let index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { - strongSelf.panelCollapseScrollToIndex = index + if let strongSelf = self { + strongSelf.panelFocusTimer?.invalidate() + var position = position + var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) + if index == nil { + position.y += 10.0 + index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) + } + if let index = index { + strongSelf.panelFocusScrollToIndex = index + } + strongSelf.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: true)) + } else { + return (inputTextState, inputMode) + } } - strongSelf.updateIsExpanded(true) } } - self.gifListView.didEndScrolling = { [weak self] in - if let strongSelf = self, false { - strongSelf.setupCollapseTimer() + self.gifListView.endedInteractiveDragging = { [weak self] position in + if let strongSelf = self { + strongSelf.panelFocusInitialPosition = position + } + } + + self.gifListView.didEndScrolling = { [weak self] decelerated in + if let strongSelf = self { + if decelerated { + strongSelf.panelFocusScrollToIndex = nil + strongSelf.panelFocusInitialPosition = nil + } + strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5) } } } @@ -1031,23 +1096,31 @@ final class ChatMediaInputNode: ChatInputNode { deinit { self.disposable.dispose() self.searchContainerNodeLoadedDisposable.dispose() - self.panelCollapseTimer?.invalidate() + self.panelFocusTimer?.invalidate() } private func updateIsExpanded(_ isExpanded: Bool) { - self.panelCollapseTimer?.invalidate() - - self.panelExpanded = isExpanded + guard self.panelIsFocused != isExpanded else { + return + } + + self.panelIsFocused = isExpanded self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring)) } - private func setupCollapseTimer() { - self.panelCollapseTimer?.invalidate() + private func setupCollapseTimer(timeout: Double) { + self.panelFocusTimer?.invalidate() - let timer = SwiftSignalKit.Timer(timeout: 1.5, repeat: false, completion: { [weak self] in - self?.updateIsExpanded(false) + let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in + self?.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } }, queue: Queue.mainQueue()) - self.panelCollapseTimer = timer + self.panelFocusTimer = timer timer.start() } @@ -1502,10 +1575,19 @@ final class ChatMediaInputNode: ChatInputNode { } itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true @@ -1513,10 +1595,19 @@ final class ChatMediaInputNode: ChatInputNode { } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true @@ -1524,10 +1615,19 @@ final class ChatMediaInputNode: ChatInputNode { } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true @@ -1535,10 +1635,19 @@ final class ChatMediaInputNode: ChatInputNode { } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true @@ -1546,10 +1655,19 @@ final class ChatMediaInputNode: ChatInputNode { } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true @@ -1562,10 +1680,19 @@ final class ChatMediaInputNode: ChatInputNode { let firstVisibleIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == firstVisibleCollectionId }) if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { let toRight = targetIndex > firstVisibleIndex - if self.panelExpanded { - self.panelCollapseScrollToIndex = targetIndex - self.updateIsExpanded(false) + if self.panelIsFocused { + self.panelFocusScrollToIndex = targetIndex + self.panelFocusInitialPosition = nil + self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in + if case let .media(mode, expanded, _) = inputMode { + return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) + } else { + return (inputTextState, inputMode) + } + } } else { + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) } } @@ -1643,7 +1770,7 @@ final class ChatMediaInputNode: ChatInputNode { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { var searchMode: ChatMediaInputSearchMode? - if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = self.validLayout, case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded { + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = self.validLayout, case let .media(_, maybeExpanded, _) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded { searchMode = mode } @@ -1659,8 +1786,12 @@ final class ChatMediaInputNode: ChatInputNode { let separatorHeight = UIScreenPixel let panelHeight: CGFloat + var isFocused = false var isExpanded: Bool = false - if case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded { + if case let .media(_, _, focused) = interfaceState.inputMode { + isFocused = focused + } + if case let .media(_, maybeExpanded, _) = interfaceState.inputMode, let expanded = maybeExpanded { isExpanded = true switch expanded { case .content: @@ -1677,6 +1808,8 @@ final class ChatMediaInputNode: ChatInputNode { panelHeight = standardInputHeight } + self.updateIsExpanded(isFocused) + if displaySearch { if let searchContainerNode = self.searchContainerNode { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) @@ -1721,10 +1854,10 @@ final class ChatMediaInputNode: ChatInputNode { transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 40.0, height: width) transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) - self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) + self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 40.0, height: width) transition.updatePosition(node: self.gifListView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -1891,15 +2024,24 @@ final class ChatMediaInputNode: ChatInputNode { } var scrollToItem: ListViewScrollToItem? - if let targetIndex = self.panelCollapseScrollToIndex { + if let targetIndex = self.panelFocusScrollToIndex { var position: ListViewScrollPosition - if self.panelExpanded { - position = .center(.top) + if self.panelIsFocused { + if let initialPosition = self.panelFocusInitialPosition { + position = .top(96.0 + (initialPosition.y - self.listView.frame.height / 2.0) * 0.5) + } else { + position = .top(96.0) + } } else { - position = .top(self.listView.frame.height / 2.0 + 96.0) + if let initialPosition = self.panelFocusInitialPosition { + position = .top(self.listView.frame.height / 2.0 + 96.0 + (initialPosition.y - self.listView.frame.height / 2.0)) + } else { + position = .top(self.listView.frame.height / 2.0 + 96.0) + } + self.panelFocusScrollToIndex = nil + self.panelFocusInitialPosition = nil } - scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Default(duration: nil), directionHint: .Down) - self.panelCollapseScrollToIndex = nil + 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 @@ -1993,7 +2135,7 @@ final class ChatMediaInputNode: ChatInputNode { private var isExpanded: Bool { var isExpanded: Bool = false - if let validLayout = self.validLayout, case let .media(_, maybeExpanded) = validLayout.8.inputMode, maybeExpanded != nil { + if let validLayout = self.validLayout, case let .media(_, maybeExpanded, _) = validLayout.8.inputMode, maybeExpanded != nil { isExpanded = true } return isExpanded @@ -2021,7 +2163,7 @@ final class ChatMediaInputNode: ChatInputNode { } var collectionListPanelOffset = self.currentCollectionListPanelOffset() - if self.panelExpanded { + if self.panelIsFocused { collectionListPanelOffset = 0.0 } @@ -2037,14 +2179,12 @@ final class ChatMediaInputNode: ChatInputNode { private func updatePaneClippingContainer(size: CGSize, offset: CGFloat, transition: ContainedViewLayoutTransition) { var offset = offset - var additionalOffset: CGFloat = 0.0 - if self.panelExpanded { + if self.panelIsFocused { offset = 0.0 - additionalOffset = 31.0 } - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: self.collectionListSeparator.bounds.size)) - transition.updateFrame(node: self.paneClippingContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: size)) - transition.updateSublayerTransformOffset(layer: self.paneClippingContainer.layer, offset: CGPoint(x: 0.0, y: -offset - 41.0 - additionalOffset)) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0), size: self.collectionListSeparator.bounds.size)) + transition.updateFrame(node: self.paneClippingContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0), size: size)) + transition.updateSublayerTransformOffset(layer: self.paneClippingContainer.layer, offset: CGPoint(x: 0.0, y: -offset - 41.0)) } private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { @@ -2059,7 +2199,7 @@ final class ChatMediaInputNode: ChatInputNode { } var collectionListPanelOffset = self.currentCollectionListPanelOffset() - if self.panelExpanded { + if self.panelIsFocused { collectionListPanelOffset = 0.0 } @@ -2073,6 +2213,15 @@ final class ChatMediaInputNode: ChatInputNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.panelIsFocused { + let convertedPoint = CGPoint(x: max(0.0, point.y), y: point.x) + if let result = self.listView.hitTest(convertedPoint, with: event) { + return result + } + if let result = self.gifListView.hitTest(convertedPoint, with: event) { + return result + } + } if let searchContainerNode = self.searchContainerNode { if let result = searchContainerNode.hitTest(point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY), with: event) { return result diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift index 37b192bbfa..0ccab940b2 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift @@ -133,16 +133,18 @@ final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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.avatarNode.position.y - titleFrame.size.height), size: titleFrame.size) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) + self.currentExpanded = expanded self.avatarNode.bounds = CGRect(origin: CGPoint(), size: imageSize) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift index 2cb3a142a2..9520e80399 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift @@ -124,16 +124,18 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) + self.currentExpanded = expanded 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)) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift index e9997e0b7f..0480a8c0bc 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift @@ -91,6 +91,7 @@ final class ChatMediaInputSettingsItemNode: ListViewItemNode { self.containerNode.addSubnode(self.scalingNode) self.scalingNode.addSubnode(self.buttonNode) + self.scalingNode.addSubnode(self.titleNode) self.scalingNode.addSubnode(self.imageNode) } @@ -114,18 +115,19 @@ final class ChatMediaInputSettingsItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) + self.currentExpanded = expanded - } func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index 85f5ad45a9..1b057d98bc 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -185,7 +185,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { var resourceReference: MediaResourceReference? if let thumbnail = info.thumbnail { if info.flags.contains(.isAnimated) { - thumbnailItem = .animated(thumbnail.resource) + thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions) resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) } else { thumbnailItem = .still(thumbnail) @@ -193,7 +193,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } } else if let item = item { if item.file.isAnimatedSticker { - thumbnailItem = .animated(item.file.resource) + thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) @@ -210,15 +210,16 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { if self.currentThumbnailItem != thumbnailItem { self.currentThumbnailItem = thumbnailItem + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) if let thumbnailItem = thumbnailItem { switch thumbnailItem { case let .still(representation): imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) - let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true)) - case let .animated(resource): - let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + case let .animated(resource, _): + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true)) @@ -236,7 +237,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } else { self.scalingNode.addSubnode(animatedStickerNode) } - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .cached) } animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers } @@ -247,7 +248,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { if let placeholderNode = self.placeholderNode { let imageSize = boundingImageSize - placeholderNode.update(backgroundColor: nil, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.4), shimmeringColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.withMultipliedAlpha(0.2), data: info.immediateThumbnailData, size: imageSize, imageSize: CGSize(width: 100.0, height: 100.0)) + placeholderNode.update(backgroundColor: nil, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.4), shimmeringColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.withMultipliedAlpha(0.2), data: info.immediateThumbnailData, size: imageSize, imageSize: thumbnailDimensions.cgSize) } self.updateIsHighlighted() @@ -259,16 +260,18 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) + self.currentExpanded = expanded self.imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift index 81de3c865e..b2d9cd33ea 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift @@ -143,14 +143,17 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate 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 ? -2.0 : 3.0))) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0))) - expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + 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) expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate + alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0) self.currentExpanded = expanded diff --git a/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift index 2a5f31271e..0f259ab762 100644 --- a/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift @@ -73,7 +73,7 @@ enum ChatMediaInputExpanded: Equatable { enum ChatInputMode: Equatable { case none case text - case media(mode: ChatMediaInputMode, expanded: ChatMediaInputExpanded?) + case media(mode: ChatMediaInputMode, expanded: ChatMediaInputExpanded?, focused: Bool) case inputButtons } diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 6e8abf40c7..310b9515f8 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -128,7 +128,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) var expanded = false - if case let .media(_, maybeExpanded) = interfaceState.inputMode, maybeExpanded != nil { + if case let .media(_, maybeExpanded, _) = interfaceState.inputMode, maybeExpanded != nil { expanded = true } transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index d54e49846a..af7a0eaf87 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -230,6 +230,7 @@ enum ChatTextInputPanelPasteData { } class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { + let clippingNode: ASDisplayNode var textPlaceholderNode: ImmediateTextNode var contextPlaceholderNode: TextNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? @@ -434,6 +435,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { init(presentationInterfaceState: ChatPresentationInterfaceState, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + self.textInputContainerBackgroundNode = ASImageNode() self.textInputContainerBackgroundNode.isUserInteractionEnabled = false self.textInputContainerBackgroundNode.displaysAsynchronously = false @@ -476,6 +480,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() + self.addSubnode(self.clippingNode) + self.menuButton.addTarget(self, action: #selector(self.menuButtonPressed), forControlEvents: .touchUpInside) self.menuButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -565,24 +571,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside) self.searchLayoutClearButton.alpha = 0.0 - self.addSubnode(self.textInputContainer) - self.addSubnode(self.textInputBackgroundNode) + self.clippingNode.addSubnode(self.textInputContainer) + self.clippingNode.addSubnode(self.textInputBackgroundNode) - self.addSubnode(self.textPlaceholderNode) + self.clippingNode.addSubnode(self.textPlaceholderNode) self.menuButton.addSubnode(self.menuButtonBackgroundNode) self.menuButton.addSubnode(self.menuButtonClippingNode) self.menuButtonClippingNode.addSubnode(self.menuButtonTextNode) self.menuButton.addSubnode(self.menuButtonIconNode) - self.addSubnode(self.menuButton) - self.addSubnode(self.attachmentButton) - self.addSubnode(self.attachmentButtonDisabledNode) + self.clippingNode.addSubnode(self.menuButton) + self.clippingNode.addSubnode(self.attachmentButton) + self.clippingNode.addSubnode(self.attachmentButtonDisabledNode) - self.addSubnode(self.actionButtons) - self.addSubnode(self.counterTextNode) + self.clippingNode.addSubnode(self.actionButtons) + self.clippingNode.addSubnode(self.counterTextNode) - self.view.addSubview(self.searchLayoutClearButton) + self.clippingNode.view.addSubview(self.searchLayoutClearButton) self.textInputBackgroundNode.clipsToBounds = true let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) @@ -1521,7 +1527,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { button.updateLayout(size: buttonSize) let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) if button.supernode == nil { - self.addSubnode(button) + self.clippingNode.addSubnode(button) button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0) transition.updateFrame(layer: button.layer, frame: buttonFrame) if animatedTransition { @@ -1645,6 +1651,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + var clippingDelta: CGFloat = 0.0 + if case let .media(_, _, focused) = interfaceState.inputMode, focused { + clippingDelta = -panelHeight + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) + transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta)) + return panelHeight } @@ -2248,11 +2261,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func expandButtonPressed() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - if case let .media(mode, expanded) = state.inputMode { + if case let .media(mode, expanded, focused) = state.inputMode { if let _ = expanded { - return (.media(mode: mode, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(mode: mode, expanded: nil, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) } else { - return (.media(mode: mode, expanded: .content), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(mode: mode, expanded: .content, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) } } else { return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 340b957869..6cc86fdb8a 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -224,6 +224,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( context: strongSelf.context, + highlightedPackId: nil, sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { @@ -263,6 +264,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { + }, openTrending: { _ in + }, dismissTrendingPacks: { _ in }, toggleSearch: { [weak self] value, searchMode, query in if let strongSelf = self { if let searchMode = searchMode, value { @@ -287,8 +290,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { if let strongSelf = self { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: .search(searchMode)) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: .search(searchMode), focused: focused) default: return current } @@ -299,8 +302,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { } else { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: nil) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: nil, focused: focused) default: return current } @@ -336,6 +339,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( context: strongSelf.context, + highlightedPackId: nil, sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { @@ -375,6 +379,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { + }, openTrending: { _ in + }, dismissTrendingPacks: { _ in }, toggleSearch: { [weak self] value, searchMode, query in if let strongSelf = self { if let searchMode = searchMode, value { @@ -399,8 +405,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { if let strongSelf = self { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: .search(searchMode)) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: .search(searchMode), focused: focused) default: return current } @@ -411,8 +417,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { } else { strongSelf.controllerInteraction.updateInputMode { current in switch current { - case let .media(mode, _): - return .media(mode: mode, expanded: nil) + case let .media(mode, _, focused): + return .media(mode: mode, expanded: nil, focused: focused) default: return current } @@ -582,20 +588,9 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { for info in view.collectionInfos { installedPacks.insert(info.0) } - - var hasUnreadTrending: Bool? - for pack in trendingPacks { - if hasUnreadTrending == nil { - hasUnreadTrending = false - } - if pack.unread { - hasUnreadTrending = true - break - } - } - - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasUnreadTrending: nil, theme: theme, hasGifs: false, hasSettings: false) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasSearch: false, hasAccessories: false, strings: strings, theme: theme) + + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, theme: theme, hasGifs: false, hasSettings: false) + let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, trendingPacks: trendingPacks, hasSearch: false, hasAccessories: false, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousStickerEntries.swap((panelEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: stickersInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: stickersInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) @@ -629,8 +624,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { installedPacks.insert(info.0) } - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasUnreadTrending: nil, theme: theme, hasGifs: false, hasSettings: false) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasSearch: false, hasAccessories: false, strings: strings, theme: theme) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, theme: theme, hasGifs: false, hasSettings: false) + let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, trendingPacks: [], hasSearch: false, hasAccessories: false, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousMaskEntries.swap((panelEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: masksInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: masksInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 4025f3ecc8..660730c184 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -95,7 +95,7 @@ private final class FeaturedPackEntry: Identifiable, Comparable { func item(account: Account, interaction: FeaturedInteraction, isOther: Bool) -> GridItem { let info = self.info - return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, listAppearance: true, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: self.regularInsets, installed: self.installed, unread: self.unread, open: { + return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, listAppearance: true, fillsRow: false, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: self.regularInsets, installed: self.installed, unread: self.unread, open: { interaction.openPack(info) }, install: { interaction.installPack(info, !self.installed) @@ -153,16 +153,17 @@ private struct FeaturedTransition { let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let initial: Bool + let scrollToItem: GridNodeScrollToItem? } -private func preparedTransition(from fromEntries: [FeaturedEntry], to toEntries: [FeaturedEntry], account: Account, interaction: FeaturedInteraction, initial: Bool) -> FeaturedTransition { +private func preparedTransition(from fromEntries: [FeaturedEntry], to toEntries: [FeaturedEntry], account: Account, interaction: FeaturedInteraction, initial: Bool, scrollToItem: GridNodeScrollToItem?) -> FeaturedTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } - return FeaturedTransition(deletions: deletions, insertions: insertions, updates: updates, initial: initial) + return FeaturedTransition(deletions: deletions, insertions: insertions, updates: updates, initial: initial, scrollToItem: scrollToItem) } private func featuredScreenEntries(featuredEntries: [FeaturedStickerPackItem], installedPacks: Set, theme: PresentationTheme, strings: PresentationStrings, fixedUnread: Set, additionalPacks: [FeaturedStickerPackItem]) -> [FeaturedEntry] { @@ -280,6 +281,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { }, openSettings: { }, + openTrending: { _ in + }, + dismissTrendingPacks: { _ in + }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { @@ -360,6 +365,8 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { return (items, fixedUnread) } + let highlightedPackId = controller.highlightedPackId + self.disposable = (combineLatest(queue: .mainQueue(), mappedFeatured, self.additionalPacks.get(), @@ -378,7 +385,20 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let entries = featuredScreenEntries(featuredEntries: featuredEntries.0, installedPacks: installedPacks, theme: presentationData.theme, strings: presentationData.strings, fixedUnread: featuredEntries.1, additionalPacks: additionalPacks) let previous = previousEntries.swap(entries) - return preparedTransition(from: previous ?? [], to: entries, account: context.account, interaction: interaction, initial: previous == nil) + var scrollToItem: GridNodeScrollToItem? + let initial = previous == nil + if initial, let highlightedPackId = highlightedPackId { + var index = 0 + for entry in entries { + if case let .pack(packEntry, _) = entry, packEntry.info.id == highlightedPackId { + scrollToItem = GridNodeScrollToItem(index: index, position: .center(0.0), transition: .immediate, directionHint: .down, adjustForSection: false) + break + } + index += 1 + } + } + + return preparedTransition(from: previous ?? [], to: entries, account: context.account, interaction: interaction, initial: initial, scrollToItem: scrollToItem) } |> deliverOnMainQueue).start(next: { [weak self] transition in guard let strongSelf = self else { @@ -682,7 +702,15 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { self.enqueuedTransitions.remove(at: 0) let itemTransition: ContainedViewLayoutTransition = .immediate - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: transition.initial), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: transition.initial), completion: { [weak self] _ in + if let strongSelf = self, transition.initial { + strongSelf.gridNode.forEachItemNode({ itemNode in + if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode, itemNode.item?.info.id == strongSelf.controller?.highlightedPackId { + itemNode.highlight() + } + }) + } + }) } } @@ -711,6 +739,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { final class FeaturedStickersScreen: ViewController { private let context: AccountContext + fileprivate let highlightedPackId: ItemCollectionId? private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? private var controllerNode: FeaturedStickersScreenNode { @@ -727,8 +756,9 @@ final class FeaturedStickersScreen: ViewController { fileprivate var searchNavigationNode: SearchNavigationContentNode? - public init(context: AccountContext, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, highlightedPackId: ItemCollectionId?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { self.context = context + self.highlightedPackId = highlightedPackId self.sendSticker = sendSticker self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -956,7 +986,7 @@ private enum FeaturedSearchEntry: Identifiable, Comparable { interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) }) case let .global(_, info, topItems, installed, topSeparator): - return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, listAppearance: false, info: info, topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: { + return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, listAppearance: true, fillsRow: true, info: info, topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: { interaction.open(info) }, install: { interaction.install(info, topItems, !installed) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 89a9f47d98..ee4c88a016 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -202,7 +202,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } } - self.historyNode.endedInteractiveDragging = { [weak self] in + self.historyNode.endedInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } @@ -615,7 +615,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } } - self.historyNode.endedInteractiveDragging = { [weak self] in + self.historyNode.endedInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 49229347a9..21b5aa76e3 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3156,7 +3156,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD break } }, sendFile: nil, - sendSticker: { f, sourceNode, sourceRect in + sendSticker: { _, _, _ in return false }, requestMessageActionUrlAuth: nil, joinVoiceChat: { peerId, invite, call in diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift b/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift index e079962089..ef4f2659e8 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift @@ -78,6 +78,7 @@ final class StickerPaneSearchGlobalItem: GridItem { let theme: PresentationTheme let strings: PresentationStrings let listAppearance: Bool + let fillsRow: Bool let info: StickerPackCollectionInfo let topItems: [StickerPackItem] let topSeparator: Bool @@ -102,14 +103,15 @@ final class StickerPaneSearchGlobalItem: GridItem { } } - return (128.0 + additionalHeight, !self.listAppearance) + return (128.0 + additionalHeight, self.fillsRow) } - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) { self.account = account self.theme = theme self.strings = strings self.listAppearance = listAppearance + self.fillsRow = fillsRow self.info = info self.topItems = topItems self.topSeparator = topSeparator @@ -155,8 +157,9 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { private let uninstallButtonNode: HighlightTrackingButtonNode private var itemNodes: [TrendingTopItemNode] private let topSeparatorNode: ASDisplayNode + private var highlightNode: ASDisplayNode? - private var item: StickerPaneSearchGlobalItem? + var item: StickerPaneSearchGlobalItem? private var appliedItem: StickerPaneSearchGlobalItem? private let preloadDisposable = MetaDisposable() private let preloadedStickerPackThumbnailDisposable = MetaDisposable() @@ -330,6 +333,27 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.canPlayMedia = item.itemContext.canPlayMedia } + func highlight() { + guard self.highlightNode == nil else { + return + } + + let highlightNode = ASDisplayNode() + highlightNode.frame = self.bounds + if let theme = self.item?.theme { + highlightNode.backgroundColor = theme.list.itemCheckColors.fillColor.withAlphaComponent(0.08) + } + self.highlightNode = highlightNode + self.insertSubnode(highlightNode, at: 0) + + Queue.mainQueue().after(1.5) { + self.highlightNode = nil + highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak highlightNode] _ in + highlightNode?.removeFromSupernode() + }) + } + } + override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { guard let item = self.item else { return diff --git a/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift new file mode 100644 index 0000000000..764ced2dba --- /dev/null +++ b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift @@ -0,0 +1,505 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import StickerResources +import ItemListStickerPackItem +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect +import MergeLists + +private let boundingSize = CGSize(width: 41.0, height: 41.0) +private let boundingImageSize = CGSize(width: 28.0, height: 28.0) + +private struct Transition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private enum EntryStableId: Hashable { + case stickerPack(Int64) +} + +private enum Entry: Comparable, Identifiable { + case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, unread: Bool, theme: PresentationTheme) + + var stableId: EntryStableId { + switch self { + case let .stickerPack(_, info, _, _, _): + return .stickerPack(info.id.id) + } + } + + static func ==(lhs: Entry, rhs: Entry) -> Bool { + switch lhs { + case let .stickerPack(index, info, topItem, lhsUnread, lhsTheme): + if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsUnread, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsUnread == rhsUnread, lhsTheme === rhsTheme { + return true + } else { + return false + } + } + } + + static func <(lhs: Entry, rhs: Entry) -> Bool { + switch lhs { + case let .stickerPack(lhsIndex, lhsInfo, _, _, _): + switch rhs { + case let .stickerPack(rhsIndex, rhsInfo, _, _, _): + if lhsIndex == rhsIndex { + return lhsInfo.id.id < rhsInfo.id.id + } else { + return lhsIndex <= rhsIndex + } + } + } + } + + func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { + switch self { + case let .stickerPack(index, info, topItem, unread, theme): + return FeaturedPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, unread: unread, index: index, theme: theme, selected: { + inputNodeInteraction.openTrending(info.id) + }) + } + } +} + +private func preparedEntryTransition(account: Account, from fromEntries: [Entry], to toEntries: [Entry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> Transition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + + return Transition(deletions: deletions, insertions: insertions, updates: updates) +} + +private func panelEntries(featuredPacks: [FeaturedStickerPackItem], theme: PresentationTheme) -> [Entry] { + var entries: [Entry] = [] + var index = 0 + for pack in featuredPacks { + entries.append(.stickerPack(index: index, info: pack.info, topItem: pack.topItems.first, unread: pack.unread, theme: theme)) + index += 1 + } + return entries +} + +private final class FeaturedPackItem: ListViewItem { + let account: Account + let inputNodeInteraction: ChatMediaInputNodeInteraction + let collectionId: ItemCollectionId + let collectionInfo: StickerPackCollectionInfo + let stickerPackItem: StickerPackItem? + let unread: Bool + let selectedItem: () -> Void + let index: Int + let theme: PresentationTheme + + var selectable: Bool { + return true + } + + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo, stickerPackItem: StickerPackItem?, unread: Bool, index: Int, theme: PresentationTheme, selected: @escaping () -> Void) { + self.account = account + self.inputNodeInteraction = inputNodeInteraction + self.collectionId = collectionId + self.collectionInfo = collectionInfo + self.stickerPackItem = stickerPackItem + self.unread = unread + self.index = index + self.theme = theme + self.selectedItem = selected + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = FeaturedPackItemNode() + node.contentSize = boundingSize + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) + node.inputNodeInteraction = self.inputNodeInteraction + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + node.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, unread: self.unread, theme: self.theme) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + completion(ListViewItemNodeLayout(contentSize: boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? FeaturedPackItemNode)?.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, unread: self.unread, theme: self.theme) + }) + } + } + + func selected(listView: ListView) { + self.selectedItem() + } +} + +private final class FeaturedPackItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let imageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode? + private let unreadNode: ASImageNode + + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var currentCollectionId: ItemCollectionId? + private var currentThumbnailItem: StickerPackThumbnailItem? + private var theme: PresentationTheme? + + private let stickerFetchedDisposable = MetaDisposable() + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false + self.animatedStickerNode?.visibility = self.visibilityStatus && loopAnimatedStickers + } + } + } + + init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = !smartInvertColorsEnabled() + + self.placeholderNode = StickerShimmerEffectNode() + + self.unreadNode = ASImageNode() + self.unreadNode.isLayerBacked = true + self.unreadNode.displayWithoutProcessing = true + self.unreadNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.containerNode) + + self.containerNode.addSubnode(self.imageNode) + if let placeholderNode = self.placeholderNode { + self.containerNode.addSubnode(placeholderNode) + } + self.containerNode.addSubnode(self.unreadNode) + + var firstTime = true + self.imageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + } + } + } + + func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, unread: Bool, theme: PresentationTheme) { + self.currentCollectionId = collectionId + + if self.theme !== theme { + self.theme = theme + } + + var thumbnailItem: StickerPackThumbnailItem? + var resourceReference: MediaResourceReference? + if let thumbnail = info.thumbnail { + if info.flags.contains(.isAnimated) { + thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } else { + thumbnailItem = .still(thumbnail) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } + } else if let item = item { + if item.file.isAnimatedSticker { + thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) + } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + } + } + + var imageSize = boundingImageSize + + if self.currentThumbnailItem != thumbnailItem { + self.currentThumbnailItem = thumbnailItem + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case let .still(representation): + imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + imageApply() + self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true)) + case let .animated(resource, _): + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + imageApply() + self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true)) + + let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false + self.imageNode.isHidden = loopAnimatedStickers + + let animatedStickerNode: AnimatedStickerNode + if let current = self.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + self.animatedStickerNode = animatedStickerNode + if let placeholderNode = self.placeholderNode { + self.containerNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode) + } else { + self.containerNode.addSubnode(animatedStickerNode) + } + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .direct(cachePathPrefix: nil)) + } + animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers + } + if let resourceReference = resourceReference { + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start()) + } + } + + if let placeholderNode = self.placeholderNode { + let imageSize = boundingImageSize + placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: info.immediateThumbnailData, size: imageSize, imageSize: thumbnailDimensions.cgSize) + } + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) + + self.imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize) + self.imageNode.position = CGPoint(x: boundingSize.height / 2.0, y: boundingSize.width / 2.0) + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.frame = self.imageNode.frame + animatedStickerNode.updateLayout(size: self.imageNode.frame.size) + } + if let placeholderNode = self.placeholderNode { + placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize) + placeholderNode.position = self.imageNode.position + } + + let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(theme) + if unread { + self.unreadNode.isHidden = false + } else { + self.unreadNode.isHidden = true + } + if let image = unreadImage { + self.unreadNode.image = image + self.unreadNode.frame = CGRect(origin: CGPoint(x: 35.0, y: 4.0), size: image.size) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let placeholderNode = self.placeholderNode { + placeholderNode.updateAbsoluteRect(rect, within: containerSize) + } + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + + +final class StickerPaneTrendingListGridItem: GridItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let trendingPacks: [FeaturedStickerPackItem] + let inputNodeInteraction: ChatMediaInputNodeInteraction + let dismiss: (() -> Void)? + + let section: GridSection? = nil + let fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, trendingPacks: [FeaturedStickerPackItem], inputNodeInteraction: ChatMediaInputNodeInteraction, dismiss: (() -> Void)?) { + self.account = account + self.theme = theme + self.strings = strings + self.trendingPacks = trendingPacks + self.inputNodeInteraction = inputNodeInteraction + self.dismiss = dismiss + self.fillsRowWithDynamicHeight = { _ in + return 70.0 + } + } + + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + let node = StickerPaneTrendingListGridItemNode() + node.setup(item: self) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPaneTrendingListGridItemNode else { + assertionFailure() + return + } + node.setup(item: self) + } +} + +private let titleFont = Font.medium(12.0) + +class StickerPaneTrendingListGridItemNode: GridItemNode { + private let titleNode: TextNode + private let dismissButtonNode: HighlightTrackingButtonNode + + private let listView: ListView + + private var item: StickerPaneTrendingListGridItem? + private var appliedItem: StickerPaneTrendingListGridItem? + + override var isVisibleInGrid: Bool { + didSet { + + } + } + + private let disposable = MetaDisposable() + private var currentEntries: [Entry] = [] + + override init() { + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.dismissButtonNode = HighlightTrackingButtonNode() + + self.listView = ListView() + self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) + self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.listView) + self.addSubnode(self.dismissButtonNode) + + self.dismissButtonNode.addTarget(self, action: #selector(self.dismissPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.disposable.dispose() + } + + private func enqueuePanelTransition(_ transition: Transition, firstTime: Bool) { + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else { + options.insert(.AnimateInsertion) + } + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateOpaqueState: nil, completion: { _ in }) + } + + func setup(item: StickerPaneTrendingListGridItem) { + self.item = item + + let entries = panelEntries(featuredPacks: item.trendingPacks, theme: item.theme) + let transition = preparedEntryTransition(account: item.account, from: self.currentEntries, to: entries, inputNodeInteraction: item.inputNodeInteraction) + self.enqueuePanelTransition(transition, firstTime: self.currentEntries.isEmpty) + self.currentEntries = entries + + self.setNeedsLayout() + } + + override func layout() { + super.layout() + guard let item = self.item else { + return + } + + let params = ListViewItemLayoutParams(width: self.bounds.size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: self.bounds.size.height) + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.appliedItem + self.appliedItem = item + + let width = self.bounds.size.width + + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) + self.listView.position = CGPoint(x: width / 2.0, y: 26.0 + 41.0 / 2.0) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: .immediate) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: self.bounds.size.width), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), duration: duration, curve: curve) + + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if currentItem?.theme !== item.theme { + self.dismissButtonNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridDismissImage(item.theme), for: []) + } + + let leftInset: CGFloat = 12.0 + let rightInset: CGFloat = 16.0 + let topOffset: CGFloat = 9.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.StickerPacksSettings_FeaturedPacks.uppercased(), font: titleFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + self.item = item + + let _ = titleApply() + + let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: topOffset), size: titleLayout.size) + let dismissButtonSize = CGSize(width: 12.0, height: 12.0) + self.dismissButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - dismissButtonSize.width, y: topOffset - 1.0), size: dismissButtonSize) + self.dismissButtonNode.isHidden = item.dismiss == nil + self.titleNode.frame = titleFrame + } + + @objc private func dismissPressed() { + if let item = self.item { + item.dismiss?() + } + } +}