Merge commit '1e0e0de7c83a5b0e9be7f7d7dbfc5f5ddce8e084'

This commit is contained in:
Ali 2021-08-10 21:18:58 +02:00
commit ee7d4b125b
50 changed files with 1774 additions and 258 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@
- (void)setZoomedProgress:(CGFloat)progress;
- (void)saveStartImage:(void (^)(void))completion;
- (TGCameraPreviewView *)previewView;
@end

View File

@ -61,6 +61,7 @@
- (instancetype)initWithContext:(id<LegacyComponentsContext>)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

View File

@ -77,5 +77,6 @@ typedef enum {
+ (UIInterfaceOrientation)_interfaceOrientationForDeviceOrientation:(UIDeviceOrientation)orientation;
+ (UIImage *)startImage;
+ (void)generateStartImageWithImage:(UIImage *)frameImage;
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, *) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Void, NoError> {
return accountManager.transaction { transaction -> Void in
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks(), ApplicationSpecificInt64ArrayNotice(values: values))
}
}
public static func reset(accountManager: AccountManager) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Bool>()
private var panelCollapseScrollToIndex: Int?
private let panelExpandedPromise = ValuePromise<Bool>(false)
private var panelExpanded: Bool = false {
private var panelFocusScrollToIndex: Int?
private var panelFocusInitialPosition: CGPoint?
private let panelIsFocusedPromise = ValuePromise<Bool>(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<ItemCollectionsView?>(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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ItemCollectionId>, theme: PresentationTheme, strings: PresentationStrings, fixedUnread: Set<ItemCollectionId>, 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)

View File

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

View File

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

View File

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

View File

@ -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<Void, NoError>?, (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?()
}
}
}