diff --git a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift index 51b7fca583..3756b899ed 100644 --- a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift @@ -14,6 +14,10 @@ public final class ContextControllerSourceNode: ASDisplayNode { public var shouldBegin: ((CGPoint) -> Bool)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? + public func cancelGesture() { + self.contextGesture?.cancel() + } + override public func didLoad() { super.didLoad() diff --git a/submodules/Display/Display/ImmediateTextNode.swift b/submodules/Display/Display/ImmediateTextNode.swift index 25d8fb69e7..f7e8dfc340 100644 --- a/submodules/Display/Display/ImmediateTextNode.swift +++ b/submodules/Display/Display/ImmediateTextNode.swift @@ -15,6 +15,7 @@ public class ImmediateTextNode: TextNode { public var insets: UIEdgeInsets = UIEdgeInsets() public var textShadowColor: UIColor? public var textStroke: (UIColor, CGFloat)? + public var cutout: TextNodeCutout? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? private var linkHighlightingNode: LinkHighlightingNode? @@ -57,7 +58,7 @@ public class ImmediateTextNode: TextNode { public func updateLayout(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self) - let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke)) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke)) let _ = apply() if layout.numberOfLines > 1 { self.trailingLineWidth = layout.trailingLineWidth @@ -69,7 +70,7 @@ public class ImmediateTextNode: TextNode { public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo { let makeLayout = TextNode.asyncLayout(self) - let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets)) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets)) let _ = apply() return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated) } diff --git a/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift index 507899560a..d78937fe04 100644 --- a/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift @@ -31,12 +31,15 @@ private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool { } } -class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { - var validatedGesture = false - var firstLocation: CGPoint = CGPoint() +public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { + private let enableBothDirections: Bool private let canBegin: () -> Bool - init(target: Any?, action: Selector?, canBegin: @escaping () -> Bool) { + var validatedGesture = false + var firstLocation: CGPoint = CGPoint() + + public init(target: Any?, action: Selector?, enableBothDirections: Bool = false, canBegin: @escaping () -> Bool) { + self.enableBothDirections = enableBothDirections self.canBegin = canBegin super.init(target: target, action: action) @@ -44,13 +47,13 @@ class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { self.maximumNumberOfTouches = 1 } - override func reset() { + override public func reset() { super.reset() validatedGesture = false } - override func touchesBegan(_ touches: Set, with event: UIEvent) { + override public func touchesBegan(_ touches: Set, with event: UIEvent) { if !self.canBegin() { self.state = .failed return @@ -68,17 +71,17 @@ class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { } } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesMoved(_ touches: Set, with event: UIEvent) { let location = touches.first!.location(in: self.view) let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) let absTranslationX: CGFloat = abs(translation.x) let absTranslationY: CGFloat = abs(translation.y) - if !validatedGesture { - if self.firstLocation.x < 16.0 { + if !self.validatedGesture { + if !self.enableBothDirections && self.firstLocation.x < 16.0 { validatedGesture = true - } else if translation.x < 0.0 { + } else if !self.enableBothDirections && translation.x < 0.0 { self.state = .failed } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { self.state = .failed diff --git a/submodules/Display/Display/NavigationBar.swift b/submodules/Display/Display/NavigationBar.swift index ff66977045..9aaa5d5b01 100644 --- a/submodules/Display/Display/NavigationBar.swift +++ b/submodules/Display/Display/NavigationBar.swift @@ -1190,14 +1190,14 @@ open class NavigationBar: ASDisplayNode { } override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { + /*if self.bounds.contains(point) { if self.backButtonNode.supernode != nil && !self.backButtonNode.isHidden { let effectiveBackButtonRect = CGRect(origin: CGPoint(), size: CGSize(width: self.backButtonNode.frame.maxX + 20.0, height: self.bounds.height)) if effectiveBackButtonRect.contains(point) { return self.backButtonNode.internalHitTest(self.view.convert(point, to: self.backButtonNode.view), with: event) } } - } + }*/ guard let result = super.hitTest(point, with: event) else { return nil diff --git a/submodules/Display/Display/TextNode.swift b/submodules/Display/Display/TextNode.swift index 1e7268cdde..2cbb4708ca 100644 --- a/submodules/Display/Display/TextNode.swift +++ b/submodules/Display/Display/TextNode.swift @@ -929,7 +929,12 @@ public class TextNode: ASDisplayNode { let coreTextLine: CTLine let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { + var lineConstrainedSize = constrainedSize + if bottomCutoutEnabled { + lineConstrainedSize.width -= bottomCutoutSize.width + } + + if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) { coreTextLine = originalLine } else { var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] @@ -939,7 +944,7 @@ public class TextNode: ASDisplayNode { let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken + coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken truncated = true } @@ -956,7 +961,7 @@ public class TextNode: ASDisplayNode { } } - let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) @@ -1032,7 +1037,7 @@ public class TextNode: ASDisplayNode { if !lines.isEmpty && bottomCutoutEnabled { let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width if proposedWidth > layoutSize.width { - if proposedWidth < constrainedSize.width { + if proposedWidth <= constrainedSize.width + .ulpOfOne { layoutSize.width = proposedWidth } else { layoutSize.height += bottomCutoutSize.height diff --git a/submodules/Display/Display/TransformImageNode.swift b/submodules/Display/Display/TransformImageNode.swift index 163b0ce437..311ce62ee1 100644 --- a/submodules/Display/Display/TransformImageNode.swift +++ b/submodules/Display/Display/TransformImageNode.swift @@ -81,7 +81,7 @@ open class TransformImageNode: ASDisplayNode { let apply: () -> Void = { if let strongSelf = self { if strongSelf.contents == nil { - if strongSelf.contentAnimations.contains(.firstUpdate) { + if strongSelf.contentAnimations.contains(.firstUpdate) && !attemptSynchronously { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } else if strongSelf.contentAnimations.contains(.subsequentUpdates) { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 11e4215545..6d5d0a7067 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -253,6 +253,33 @@ private enum GalleryMessageHistoryView { return [entry] } } + + var tagMask: MessageTags? { + switch self { + case .single: + return nil + case let .view(view): + return view.tagMask + } + } + + var hasEarlier: Bool { + switch self { + case .single: + return false + case let .view(view): + return view.earlierId != nil + } + } + + var hasLater: Bool { + switch self { + case .single: + return false + case let .view(view): + return view.laterId != nil + } + } } public enum GalleryControllerItemSource { @@ -304,6 +331,7 @@ public class GalleryController: ViewController, StandalonePresentableController private let context: AccountContext private var presentationData: PresentationData private let source: GalleryControllerItemSource + private let invertItemOrder: Bool private let streamVideos: Bool @@ -324,6 +352,9 @@ public class GalleryController: ViewController, StandalonePresentableController private let disposable = MetaDisposable() private var entries: [MessageHistoryEntry] = [] + private var hasLeftEntries: Bool = false + private var hasRightEntries: Bool = false + private var tagMask: MessageTags? private var centralEntryStableId: UInt32? private var configuration: GalleryConfiguration? @@ -346,9 +377,12 @@ public class GalleryController: ViewController, StandalonePresentableController private var performAction: (GalleryControllerInteractionTapAction) -> Void private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void + private let updateVisibleDisposable = MetaDisposable() + public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { self.context = context self.source = source + self.invertItemOrder = invertItemOrder self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController self.actionInteraction = actionInteraction @@ -444,13 +478,19 @@ public class GalleryController: ViewController, StandalonePresentableController } } + strongSelf.tagMask = view.tagMask + if invertItemOrder { strongSelf.entries = entries.reversed() + strongSelf.hasLeftEntries = view.hasLater + strongSelf.hasRightEntries = view.hasEarlier if let centralEntryStableId = centralEntryStableId { strongSelf.centralEntryStableId = centralEntryStableId } } else { strongSelf.entries = entries + strongSelf.hasLeftEntries = view.hasEarlier + strongSelf.hasRightEntries = view.hasLater strongSelf.centralEntryStableId = centralEntryStableId } if strongSelf.isViewLoaded { @@ -774,6 +814,7 @@ public class GalleryController: ViewController, StandalonePresentableController if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex { self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) } + self.updateVisibleDisposable.dispose() } @objc private func donePressed() { @@ -898,6 +939,7 @@ public class GalleryController: ViewController, StandalonePresentableController var hiddenItem: (MessageId, Media)? if let index = index { let message = strongSelf.entries[index].message + strongSelf.centralEntryStableId = message.stableId if let (media, _) = mediaForMessage(message: message) { hiddenItem = (message.id, media) } @@ -910,6 +952,69 @@ public class GalleryController: ViewController, StandalonePresentableController strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } + + switch strongSelf.source { + case let .peerMessagesAtId(initialMessageId): + var reloadAroundIndex: MessageIndex? + if index <= 2 && strongSelf.hasLeftEntries { + reloadAroundIndex = strongSelf.entries.first?.index + } else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries { + reloadAroundIndex = strongSelf.entries.last?.index + } + if let reloadAroundIndex = reloadAroundIndex, let tagMask = strongSelf.tagMask { + let namespaces: MessageIdNamespaces + if Namespaces.Message.allScheduled.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allScheduled) + } else { + namespaces = .not(Namespaces.Message.allScheduled) + } + let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(initialMessageId.peerId), anchor: .index(reloadAroundIndex), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [.combinedLocation]) + |> mapToSignal { (view, _, _) -> Signal in + let mapped = GalleryMessageHistoryView.view(view) + return .single(mapped) + } + |> take(1) + + strongSelf.updateVisibleDisposable.set((signal + |> deliverOnMainQueue).start(next: { view in + guard let strongSelf = self, let view = view else { + return + } + + let entries = view.entries + + if strongSelf.invertItemOrder { + strongSelf.entries = entries.reversed() + strongSelf.hasLeftEntries = view.hasLater + strongSelf.hasRightEntries = view.hasEarlier + } else { + strongSelf.entries = entries + strongSelf.hasLeftEntries = view.hasEarlier + strongSelf.hasRightEntries = view.hasLater + } + if strongSelf.isViewLoaded { + var items: [GalleryItem] = [] + var centralItemIndex: Int? + for entry in strongSelf.entries { + var isCentral = false + if entry.message.stableId == strongSelf.centralEntryStableId { + isCentral = true + } + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }) { + if isCentral { + centralItemIndex = items.count + } + items.append(item) + } + } + + strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) + } + })) + } + default: + break + } } if strongSelf.didSetReady { strongSelf._hiddenMedia.set(.single(hiddenItem)) diff --git a/submodules/GalleryUI/Sources/GalleryItem.swift b/submodules/GalleryUI/Sources/GalleryItem.swift index 89cfa918e5..c0b41d8194 100644 --- a/submodules/GalleryUI/Sources/GalleryItem.swift +++ b/submodules/GalleryUI/Sources/GalleryItem.swift @@ -21,6 +21,8 @@ public struct GalleryItemIndexData: Equatable { } public protocol GalleryItem { + var id: AnyHashable { get } + func node() -> GalleryItemNode func updateNode(node: GalleryItemNode) func thumbnailItem() -> (Int64, GalleryThumbnailItem)? diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 1b2f7e5649..d0bba72d76 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -152,16 +152,27 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } public func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, keepFirst: Bool = false) { + var items = items + if keepFirst && !self.items.isEmpty && !items.isEmpty { + items[0] = self.items[0] + } + var updateItems: [GalleryPagerUpdateItem] = [] - let deleteItems: [Int] = [] + var deleteItems: [Int] = [] var insertItems: [GalleryPagerInsertItem] = [] - for i in 0 ..< items.count { - if i == 0 && keepFirst { - updateItems.append(GalleryPagerUpdateItem(index: 0, previousIndex: 0, item: items[i])) - } else { - insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: nil)) + var previousIndexById: [AnyHashable: Int] = [:] + var validIds = Set(items.map { $0.id }) + + for i in 0 ..< self.items.count { + previousIndexById[self.items[i].id] = i + if !validIds.contains(self.items[i].id) { + deleteItems.append(i) } } + + for i in 0 ..< items.count { + insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: previousIndexById[items[i].id])) + } self.transaction(GalleryPagerTransaction(deleteItems: deleteItems, insertItems: insertItems, updateItems: updateItems, focusOnItem: centralItemIndex)) } @@ -169,6 +180,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { for updatedItem in transaction.updateItems { self.items[updatedItem.previousIndex] = updatedItem.item if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) { + //print("update visible node at \(updatedItem.previousIndex)") updatedItem.item.updateNode(node: itemNode) } } @@ -180,55 +192,52 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { self.items.remove(at: deleteItemIndex) for i in 0 ..< self.itemNodes.count { if self.itemNodes[i].index == deleteItemIndex { + //print("delete visible node at \(deleteItemIndex)") self.removeVisibleItemNode(internalIndex: i) break } } } - for itemNode in self.itemNodes { - var indexOffset = 0 - for deleteIndex in deleteItems { - if deleteIndex < itemNode.index { - indexOffset += 1 - } else { - break - } - } - - itemNode.index = itemNode.index - indexOffset - } - let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) - if self.items.count == 0 && !insertItems.isEmpty { - if insertItems[0].index != 0 { - fatalError("transaction: invalid insert into empty list") - } + + if transaction.updateItems.isEmpty && !insertItems.isEmpty { + self.items.removeAll() } for insertedItem in insertItems { - self.items.insert(insertedItem.item, at: insertedItem.index) + self.items.append(insertedItem.item) + //self.items.insert(insertedItem.item, at: insertedItem.index) } - let sortedInsertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + let visibleIndices: [Int] = self.itemNodes.map { $0.index } + + var remapIndices: [Int: Int] = [:] + for i in 0 ..< insertItems.count { + if let previousIndex = insertItems[i].previousIndex, visibleIndices.contains(previousIndex) { + remapIndices[previousIndex] = i + } + } for itemNode in self.itemNodes { - var indexOffset = 0 - for insertedItem in sortedInsertItems { - if insertedItem.index <= itemNode.index + indexOffset { - indexOffset += 1 - } + if let remappedIndex = remapIndices[itemNode.index] { + //print("remap visible node \(itemNode.index) -> \(remappedIndex)") + itemNode.index = remappedIndex } - - itemNode.index = itemNode.index + indexOffset } + self.itemNodes.sort(by: { $0.index < $1.index }) + + //print("visible indices before update \(self.itemNodes.map { $0.index })") + self.invalidatedItems = true if let focusOnItem = transaction.focusOnItem { self.centralItemIndex = focusOnItem } self.updateItemNodes(transition: .immediate) + + //print("visible indices after update \(self.itemNodes.map { $0.index })") } else if let focusOnItem = transaction.focusOnItem { self.ignoreCentralItemIndexUpdate = true diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index 834d6758d1..6c5288df31 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -15,6 +15,10 @@ import StickerResources import AppBundle class ChatAnimationGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index 2be4d488cc..cb04f53db0 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -12,6 +12,10 @@ import AccountContext import RadialStatusNode class ChatDocumentGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index 753b5cc904..bfcdf0fb42 100644 --- a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift @@ -13,6 +13,10 @@ import RadialStatusNode import ShareController class ChatExternalFileGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index e07033342e..23c5b815cb 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -79,6 +79,10 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { } class ChatImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index f0b6c51d16..55d4fad945 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -19,6 +19,10 @@ public enum UniversalVideoGalleryItemContentInfo { } public class UniversalVideoGalleryItem: GalleryItem { + public var id: AnyHashable { + return self.content.id + } + let context: AccountContext let presentationData: PresentationData let content: UniversalVideoContent diff --git a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift index 1d0c082ae5..087b35790e 100644 --- a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift +++ b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift @@ -35,6 +35,12 @@ private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem { } class InstantImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.itemId + } + + let itemId: AnyHashable + let context: AccountContext let presentationData: PresentationData let imageReference: ImageMediaReference @@ -44,7 +50,8 @@ class InstantImageGalleryItem: GalleryItem { let openUrl: (InstantPageUrlItem) -> Void let openUrlOptions: (InstantPageUrlItem) -> Void - init(context: AccountContext, presentationData: PresentationData, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, presentationData: PresentationData, itemId: AnyHashable, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { + self.itemId = itemId self.context = context self.presentationData = presentationData self.imageReference = imageReference diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index d50fc2b478..6c7ef95f88 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -98,7 +98,7 @@ public struct InstantPageGalleryEntry: Equatable { } if let image = self.media.media as? TelegramMediaImage { - return InstantImageGalleryItem(context: context, presentationData: presentationData, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } else if let file = self.media.media as? TelegramMediaFile { if file.isVideo { var indexData: GalleryItemIndexData? @@ -121,7 +121,7 @@ public struct InstantPageGalleryEntry: Equatable { representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource)) } let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - return InstantImageGalleryItem(context: context, presentationData: presentationData, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } } else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content { if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) { diff --git a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift index b2604b8881..c7f7ccdf4c 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift @@ -31,7 +31,7 @@ struct SecureIdDocumentGalleryEntry: Equatable { } func item(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, delete: @escaping (TelegramMediaResource) -> Void) -> GalleryItem { - return SecureIdDocumentGalleryItem(context: context, theme: theme, strings: strings, secureIdContext: secureIdContext, resource: self.resource, caption: self.error, location: self.location, delete: { + return SecureIdDocumentGalleryItem(context: context, theme: theme, strings: strings, secureIdContext: secureIdContext, itemId: self.index, resource: self.resource, caption: self.error, location: self.location, delete: { delete(self.resource) }) } diff --git a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift index 857c233cc3..d1315f41a7 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift @@ -12,6 +12,12 @@ import PhotoResources import GalleryUI class SecureIdDocumentGalleryItem: GalleryItem { + var id: AnyHashable { + return self.itemId + } + + let itemId: AnyHashable + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings @@ -21,7 +27,8 @@ class SecureIdDocumentGalleryItem: GalleryItem { let location: SecureIdDocumentGalleryEntryLocation let delete: () -> Void - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, itemId: AnyHashable, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) { + self.itemId = itemId self.context = context self.theme = theme self.strings = strings diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 2e42e1f680..1035a78876 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -11,15 +11,29 @@ import TelegramPresentationData import AccountContext import GalleryUI +public enum AvatarGalleryEntryId: Hashable { + case topImage + case image(MediaId) +} + public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], GalleryItemIndexData?) - case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer?, Int32, GalleryItemIndexData?, MessageId?) + case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer?, Int32, GalleryItemIndexData?, MessageId?) + + public var id: AvatarGalleryEntryId { + switch self { + case .topImage: + return .topImage + case let .image(image): + return .image(image.0) + } + } public var representations: [ImageRepresentationWithReference] { switch self { case let .topImage(representations, _): return representations - case let .image(_, representations, _, _, _, _): + case let .image(_, _, representations, _, _, _, _): return representations } } @@ -28,7 +42,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, indexData): return indexData - case let .image(_, _, _, _, indexData, _): + case let .image(_, _, _, _, _, indexData, _): return indexData } } @@ -41,8 +55,8 @@ public enum AvatarGalleryEntry: Equatable { } else { return false } - case let .image(lhsImageReference, lhsRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId): - if case let .image(rhsImageReference, rhsRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId) = rhs, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId { + case let .image(lhsId, lhsImageReference, lhsRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId): + if case let .image(rhsId, rhsImageReference, rhsRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId) = rhs, lhsId == rhsId, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId { return true } else { return false @@ -84,9 +98,9 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) } else { - result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) } index += 1 } @@ -111,9 +125,9 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) } else { - result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) } index += 1 } @@ -130,6 +144,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let context: AccountContext private let peer: Peer + private let sourceHasRoundCorners: Bool private var presentationData: PresentationData @@ -159,12 +174,15 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let replaceRootController: (ViewController, ValuePromise?) -> Void - public init(context: AccountContext, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { + public init(context: AccountContext, peer: Peer, sourceHasRoundCorners: Bool = true, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { self.context = context self.peer = peer + self.sourceHasRoundCorners = sourceHasRoundCorners self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController + self.centralEntryIndex = centralEntryIndex + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) @@ -196,7 +214,9 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr let f: () -> Void = { if let strongSelf = self { strongSelf.entries = entries - strongSelf.centralEntryIndex = 0 + if strongSelf.centralEntryIndex == nil { + strongSelf.centralEntryIndex = 0 + } if strongSelf.isViewLoaded { let canDelete: Bool if strongSelf.peer.id == strongSelf.context.account.peerId { @@ -213,7 +233,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } else { canDelete = false } - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, delete: canDelete ? { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceHasRoundCorners: sourceHasRoundCorners, delete: canDelete ? { self?.deleteEntry(entry) } : nil) }), centralItemIndex: 0, keepFirst: true) @@ -296,7 +316,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { if !self.entries.isEmpty { - if centralItemNode.index == 0, let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { + if (centralItemNode.index == 0 || !self.sourceHasRoundCorners), let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { animatedOutNode = false centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { animatedOutNode = true @@ -333,7 +353,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr self.galleryNode.transitionDataForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? AvatarGalleryControllerPresentationArguments { - if centralItemNode.index != 0 { + if centralItemNode.index != 0 && strongSelf.sourceHasRoundCorners { return nil } if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { @@ -365,7 +385,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } let presentationData = self.presentationData - self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, delete: canDelete ? { [weak self] in + self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, sourceHasRoundCorners: self.sourceHasRoundCorners, delete: canDelete ? { [weak self] in self?.deleteEntry(entry) } : nil) }), centralItemIndex: self.centralEntryIndex) @@ -469,7 +489,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } } - case let .image(reference, _, _, _, _, messageId): + case let .image(_, reference, _, _, _, _, messageId): if self.peer.id == self.context.account.peerId { if let reference = reference { let _ = removeAccountPhoto(network: self.context.account.network, reference: reference).start() diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 01cc3e6394..5bfda46d5a 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -84,7 +84,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { var nameText: String? var dateText: String? switch entry { - case let .image(_, _, peer, date, _, _): + case let .image(_, _, _, peer, date, _, _): nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date) default: diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 995ce56263..45415eef19 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -42,22 +42,28 @@ private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { } class PeerAvatarImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.entry.id + } + let context: AccountContext let peer: Peer let presentationData: PresentationData let entry: AvatarGalleryEntry + let sourceHasRoundCorners: Bool let delete: (() -> Void)? - init(context: AccountContext, peer: Peer, presentationData: PresentationData, entry: AvatarGalleryEntry, delete: (() -> Void)?) { + init(context: AccountContext, peer: Peer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceHasRoundCorners: Bool, delete: (() -> Void)?) { self.context = context self.peer = peer self.presentationData = presentationData self.entry = entry + self.sourceHasRoundCorners = sourceHasRoundCorners self.delete = delete } func node() -> GalleryItemNode { - let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer) + let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer, sourceHasRoundCorners: self.sourceHasRoundCorners) if let indexData = self.entry.indexData { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0)) @@ -85,7 +91,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { switch self.entry { case let .topImage(representations, _): content = representations - case let .image(_, representations, _, _, _, _): + case let .image(_, _, representations, _, _, _, _): content = representations } @@ -96,6 +102,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private let peer: Peer + private let sourceHasRoundCorners: Bool private var entry: AvatarGalleryEntry? @@ -110,9 +117,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private var status: MediaResourceStatus? - init(context: AccountContext, presentationData: PresentationData, peer: Peer) { + init(context: AccountContext, presentationData: PresentationData, peer: Peer, sourceHasRoundCorners: Bool) { self.context = context self.peer = peer + self.sourceHasRoundCorners = sourceHasRoundCorners self.imageNode = TransformImageNode() self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData) @@ -175,7 +183,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { switch entry { case let .topImage(topRepresentations, _): representations = topRepresentations - case let .image(_, imageRepresentations, _, _, _, _): + case let .image(_, _, imageRepresentations, _, _, _, _): representations = imageRepresentations } self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations), dispatchOnDisplayLink: false) @@ -235,10 +243,44 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) + let scaledLocalImageViewBounds = self.imageNode.view.bounds - let copyView = node.2().0! + let copyViewContents = node.2().0! + let copyView = UIView() + copyView.addSubview(copyViewContents) + copyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSelfFrame.width - copyViewContents.frame.width) / 2.0, y: (transformedSelfFrame.height - copyViewContents.frame.height) / 2.0), size: copyViewContents.frame.size) + copyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSelfFrame.width / copyViewContents.frame.width, transformedSelfFrame.height / copyViewContents.frame.height, 1.0) - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + let surfaceCopyViewContents = node.2().0! + let surfaceCopyView = UIView() + surfaceCopyView.addSubview(surfaceCopyViewContents) + + addToTransitionSurface(surfaceCopyView) + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceFinalFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceFinalFrame = self.imageNode.view.convert(scaledLocalImageViewBounds, to: contentSurface) + } + + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { + surfaceCopyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSurfaceFrame.width - surfaceCopyViewContents.frame.width) / 2.0, y: (transformedSurfaceFrame.height - surfaceCopyViewContents.frame.height) / 2.0), size: surfaceCopyViewContents.frame.size) + surfaceCopyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSurfaceFrame.width / surfaceCopyViewContents.frame.width, transformedSurfaceFrame.height / surfaceCopyViewContents.frame.height, 1.0) + surfaceCopyView.frame = transformedSurfaceFrame + + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedSurfaceFinalFrame.midX, y: transformedSurfaceFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFrame.size.height / transformedSelfFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + + surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in + surfaceCopyView?.removeFromSuperview() + }) + } + + if self.sourceHasRoundCorners { + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + } copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -259,11 +301,13 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.imageNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) self.imageNode.clipsToBounds = true - self.imageNode.layer.animate(from: (self.imageNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in - if value { - self?.imageNode.clipsToBounds = false - } - }) + if self.sourceHasRoundCorners { + self.imageNode.layer.animate(from: (self.imageNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in + if value { + self?.imageNode.clipsToBounds = false + } + }) + } self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) @@ -279,20 +323,49 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { var positionCompleted = false var boundsCompleted = false var copyCompleted = false + var surfaceCopyCompleted = false let copyView = node.2().0! - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + if self.sourceHasRoundCorners { + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + } copyView.frame = transformedSelfFrame - let intermediateCompletion = { [weak copyView] in + let surfaceCopyView = node.2().0! + if !self.sourceHasRoundCorners { + addToTransitionSurface(surfaceCopyView) + } + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceCopyViewInitialFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: contentSurface) + } + + let durationFactor = 1.0 + + let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in if positionCompleted && boundsCompleted && copyCompleted { copyView?.removeFromSuperview() + surfaceCopyView?.removeFromSuperview() completion() } } - let durationFactor = 1.0 + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame { + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false) + + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceCopyViewInitialFrame.midX, y: transformedSurfaceCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedSurfaceCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in + surfaceCopyCompleted = true + intermediateCompletion() + }) + } else { + surfaceCopyCompleted = true + } copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false) @@ -319,7 +392,9 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { }) self.imageNode.clipsToBounds = true - self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false) + if self.sourceHasRoundCorners { + self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false) + } self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) @@ -343,7 +418,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { switch entry { case let .topImage(topRepresentations, _): representations = topRepresentations - case let .image(_, imageRepresentations, _, _, _, _): + case let .image(_, _, imageRepresentations, _, _, _, _): representations = imageRepresentations } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift index de7ea72d05..0157a7c7ba 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift @@ -297,7 +297,7 @@ public class WallpaperGalleryController: ViewController { var i: Int = 0 var updateItems: [GalleryPagerUpdateItem] = [] for entry in entries { - let item = GalleryPagerUpdateItem(index: i, previousIndex: i, item: WallpaperGalleryItem(context: self.context, entry: entry, arguments: arguments, source: self.source)) + let item = GalleryPagerUpdateItem(index: i, previousIndex: i, item: WallpaperGalleryItem(context: self.context, index: updateItems.count, entry: entry, arguments: arguments, source: self.source)) updateItems.append(item) i += 1 } @@ -660,7 +660,7 @@ public class WallpaperGalleryController: ViewController { colors = true } - self.galleryNode.pager.replaceItems(self.entries.map({ WallpaperGalleryItem(context: self.context, entry: $0, arguments: WallpaperGalleryItemArguments(isColorsList: colors), source: self.source) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: WallpaperGalleryItemArguments(isColorsList: colors), source: self.source) }), centralItemIndex: self.centralEntryIndex) if let initialOptions = self.initialOptions, let itemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { itemNode.options = initialOptions diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index a159690102..07774073d6 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -32,13 +32,20 @@ struct WallpaperGalleryItemArguments { } class WallpaperGalleryItem: GalleryItem { + var id: AnyHashable { + return self.index + } + + let index: Int + let context: AccountContext let entry: WallpaperGalleryEntry let arguments: WallpaperGalleryItemArguments let source: WallpaperListSource - init(context: AccountContext, entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments, source: WallpaperListSource) { + init(context: AccountContext, index: Int, entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments, source: WallpaperListSource) { self.context = context + self.index = index self.entry = entry self.arguments = arguments self.source = source diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 5b48c84791..942f1d6015 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1954,6 +1954,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in f(.dismissWithoutContent) self?.navigationButtonAction(.openChatInfo(expandAvatar: true)) + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_Search, icon: { _ in nil }, action: { _, f in + f(.dismissWithoutContent) + self?.interfaceInteraction?.beginMessageSearch(.everything, "") })) ] let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 12964d2a8b..f718377b9a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -77,11 +77,11 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } if case .standard(true) = presentationInterfaceState.mode { - return nil + return chatInfoNavigationButton } else if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { if presentationInterfaceState.isScheduledMessages { - return nil + return chatInfoNavigationButton } else { let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Conversation_Search diff --git a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift index 6ff8ed7f57..268183aae1 100644 --- a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift +++ b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift @@ -46,7 +46,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message: switch action.action { case let .photoUpdated(image): if let peer = messageMainPeer(message), let image = image { - let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id)]) + let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id)]) let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: promise, replaceRootController: { controller, ready in }) diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index 33a0ebfdac..7bace55171 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -23,6 +23,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let action: (() -> Void)? let longTapAction: ((ASDisplayNode) -> Void)? let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? + let requestLayout: () -> Void init( id: AnyHashable, @@ -32,7 +33,8 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, action: (() -> Void)?, longTapAction: ((ASDisplayNode) -> Void)? = nil, - linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil + linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, + requestLayout: @escaping () -> Void ) { self.id = id self.label = label @@ -42,6 +44,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { self.action = action self.longTapAction = longTapAction self.linkItemAction = linkItemAction + self.requestLayout = requestLayout } func node() -> PeerInfoScreenItemNode { @@ -55,11 +58,16 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private let textNode: ImmediateTextNode private let bottomSeparatorNode: ASDisplayNode + private let expandNode: ImmediateTextNode + private let expandButonNode: HighlightTrackingButtonNode + private var linkHighlightingNode: LinkHighlightingNode? private var item: PeerInfoScreenLabeledValueItem? private var theme: PresentationTheme? + private var isExpanded: Bool = false + override init() { var bringToFrontForHighlightImpl: (() -> Void)? self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) @@ -76,6 +84,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.isLayerBacked = true + self.expandNode = ImmediateTextNode() + self.expandNode.displaysAsynchronously = false + self.expandNode.isUserInteractionEnabled = false + + self.expandButonNode = HighlightTrackingButtonNode() + super.init() bringToFrontForHighlightImpl = { [weak self] in @@ -86,6 +100,27 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { self.addSubnode(self.selectionNode) self.addSubnode(self.labelNode) self.addSubnode(self.textNode) + + self.addSubnode(self.expandNode) + self.addSubnode(self.expandButonNode) + + self.expandButonNode.addTarget(self, action: #selector(self.expandPressed), forControlEvents: .touchUpInside) + self.expandButonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.expandNode.layer.removeAnimation(forKey: "opacity") + strongSelf.expandNode.alpha = 0.4 + } else { + strongSelf.expandNode.alpha = 1.0 + strongSelf.expandNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func expandPressed() { + self.isExpanded = true + self.item?.requestLayout() } override func didLoad() { @@ -96,6 +131,9 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { guard let strongSelf = self, let item = strongSelf.item else { return .keepWithSingleTap } + if !strongSelf.expandButonNode.isHidden, strongSelf.expandButonNode.view.hitTest(strongSelf.view.convert(point, to: strongSelf.expandButonNode.view), with: nil) != nil { + return .fail + } if let _ = strongSelf.linkItemAtPoint(point) { return .waitForSingleTap } @@ -162,14 +200,19 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { textColorValue = presentationData.theme.list.itemAccentColor } + self.expandNode.attributedText = NSAttributedString(string: "more", font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor) + let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0)) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) switch item.textBehavior { case .singleLine: + self.textNode.cutout = nil self.textNode.maximumNumberOfLines = 1 self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) case let .multiLine(maxLines, enabledEntities): - self.textNode.maximumNumberOfLines = maxLines + self.textNode.maximumNumberOfLines = self.isExpanded ? maxLines : 2 + self.textNode.cutout = self.isExpanded ? nil : TextNodeCutout(bottomRight: CGSize(width: expandSize.width + 4.0, height: expandSize.height)) if enabledEntities.isEmpty { self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) } else { @@ -188,11 +231,24 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) - let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let textSize = textLayout.size + + if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded { + self.expandNode.isHidden = false + self.expandButonNode.isHidden = false + } else { + self.expandNode.isHidden = true + self.expandButonNode.isHidden = true + } let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize) let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize) + let expandFrame = CGRect(origin: CGPoint(x: textFrame.minX + max(self.textNode.trailingLineWidth ?? 0.0, textFrame.width) - expandSize.width, y: textFrame.maxY - expandSize.height), size: expandSize) + self.expandNode.frame = expandFrame + self.expandButonNode.frame = expandFrame.insetBy(dx: -8.0, dy: -8.0) + transition.updateFrame(node: self.labelNode, frame: labelFrame) transition.updateFrame(node: self.textNode, frame: textFrame) diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index e655f28ab3..20b57f6b1a 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -133,8 +133,18 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var scrollToItem: ListViewScrollToItem? + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0): + break + default: + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) + } + } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift index b021f9542c..542cf9e8ca 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -77,6 +77,14 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0): + break + default: + self.listNode.scrollToEndOfHistory() + } + } self.listNode.scrollEnabled = !isScrollingLockedAtTop } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift index f22ca30ca6..cb1ca76b2c 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -177,7 +177,16 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + var scrollToItem: ListViewScrollToItem? + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0): + break + default: + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) + } + } + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index c513709254..f789aa02d1 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -106,6 +106,10 @@ private final class VisualMediaItemNode: ASDisplayNode { } } + func cancelPreviewGesture() { + self.containerNode.cancelGesture() + } + func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) { if item === self.item?.0 && size == self.item?.2 { return @@ -553,7 +557,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous) if isScrollingLockedAtTop { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + if self.scrollNode.view.contentOffset.y > .ulpOfOne { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + } } self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop } @@ -561,6 +567,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.decelerationAnimator?.isPaused = true self.decelerationAnimator = nil + + for (_, itemNode) in self.visibleMediaItems { + itemNode.cancelPreviewGesture() + } } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift index e5c1f21c19..65653a3066 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift @@ -168,7 +168,7 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode { super.init() - self.imageNode.contentAnimations = .subsequentUpdates + self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] self.addSubnode(self.imageNode) self.imageNode.imageUpdated = { [weak self] _ in @@ -242,6 +242,14 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { } } + var currentEntry: AvatarGalleryEntry? { + if self.currentIndex >= 0 && self.currentIndex < self.galleryEntries.count { + return self.galleryEntries[self.currentIndex] + } else { + return nil + } + } + init(context: AccountContext) { self.context = context @@ -406,7 +414,15 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { func selectFirstItem() { self.currentIndex = 0 if let size = self.validLayout { - self.updateItems(size: size, transition: .immediate) + self.updateItems(size: size, transition: .immediate, stripTransition: .immediate) + } + } + + func updateEntryIsHidden(entry: AvatarGalleryEntry?) { + if let entry = entry, let index = self.galleryEntries.index(of: entry) { + self.currentItemNode?.isHidden = index == self.currentIndex + } else { + self.currentItemNode?.isHidden = false } } @@ -418,18 +434,18 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { if location.x < size.width * 1.0 / 5.0 { if self.currentIndex != 0 { self.currentIndex -= 1 - self.updateItems(size: size, transition: .immediate) + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring)) } else if self.items.count > 1 { self.currentIndex = self.items.count - 1 - self.updateItems(size: size, transition: .immediate, synchronous: true) + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) } } else { if self.currentIndex < self.items.count - 1 { self.currentIndex += 1 - self.updateItems(size: size, transition: .immediate) + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring)) } else if self.items.count > 1 { self.currentIndex = 0 - self.updateItems(size: size, transition: .immediate, synchronous: true) + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) } } } @@ -452,7 +468,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { } self.transitionFraction = transitionFraction if let size = self.validLayout { - self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring)) + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring)) } case .cancelled, .ended: let translation = recognizer.translation(in: self.view) @@ -472,7 +488,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.currentIndex = updatedIndex self.transitionFraction = 0.0 if let size = self.validLayout { - self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring)) + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring)) } default: break @@ -497,14 +513,14 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { switch entry { case let .topImage(representations, _): items.append(.topImage(representations)) - case let .image(reference, representations, _, _, _, _): + case let .image(_, reference, representations, _, _, _, _): items.append(.image(reference, representations)) } } strongSelf.galleryEntries = entries strongSelf.items = items if let size = strongSelf.validLayout { - strongSelf.updateItems(size: size, transition: .immediate) + strongSelf.updateItems(size: size, transition: .immediate, stripTransition: .immediate) } if items.isEmpty { if !strongSelf.didSetReady { @@ -514,10 +530,10 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { } })) } - self.updateItems(size: size, transition: transition) + self.updateItems(size: size, transition: transition, stripTransition: transition) } - private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition, synchronous: Bool = false) { + private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition, stripTransition: ContainedViewLayoutTransition, synchronous: Bool = false) { var validIds: [WrappedMediaResourceId] = [] var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = [] var additiveTransitionOffset: CGFloat = 0.0 @@ -603,15 +619,20 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { let stripInset: CGFloat = 8.0 let stripSpacing: CGFloat = 4.0 let stripWidth: CGFloat = max(5.0, floor((size.width - stripInset * 2.0 - stripSpacing * CGFloat(self.stripNodes.count - 1)) / CGFloat(self.stripNodes.count))) - var stripX: CGFloat = stripInset + let currentStripMinX = stripInset + CGFloat(self.currentIndex) * (stripWidth + stripSpacing) + let currentStripMidX = floor(currentStripMinX + stripWidth / 2.0) + let lastStripMaxX = stripInset + CGFloat(self.stripNodes.count - 1) * (stripWidth + stripSpacing) + stripWidth + let maxStripOffset: CGFloat = 0.0 + let stripOffset: CGFloat = min(0.0, max(size.width - stripInset - lastStripMaxX, size.width / 2.0 - currentStripMidX)) for i in 0 ..< self.stripNodes.count { + let stripX: CGFloat = stripInset + CGFloat(i) * (stripWidth + stripSpacing) if i == 0 && self.stripNodes.count == 1 { self.stripNodes[i].isHidden = true } else { self.stripNodes[i].isHidden = false } - self.stripNodes[i].frame = CGRect(origin: CGPoint(x: stripX, y: 0.0), size: CGSize(width: stripWidth + 1.0, height: 2.0)) - stripX += stripWidth + stripSpacing + let stripFrame = CGRect(origin: CGPoint(x: stripOffset + stripX, y: 0.0), size: CGSize(width: stripWidth + 1.0, height: 2.0)) + stripTransition.updateFrame(node: self.stripNodes[i], frame: stripFrame) } if let item = self.items.first, let itemNode = self.itemNodes[item.id] { @@ -1047,8 +1068,10 @@ protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat } -final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode { +final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate { private let textNode: TextFieldNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode private let topSeparator: ASDisplayNode private var theme: PresentationTheme? @@ -1059,20 +1082,69 @@ final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeader override init() { self.textNode = TextFieldNode() + + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + self.topSeparator = ASDisplayNode() super.init() self.addSubnode(self.textNode) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) self.addSubnode(self.topSeparator) + + self.textNode.textField.delegate = self + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + self.textNode.textField.text = "" + self.updateClearButtonVisibility() + } + + @objc func textFieldDidBeginEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden } func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { if self.theme !== presentationData.theme { self.theme = presentationData.theme self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor - //self.textNode.textField.keyboardAppearance = presentationData.theme.keyboardAppearance + self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) } let attributedPlaceholderText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor) @@ -1090,7 +1162,13 @@ final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeader let height: CGFloat = 44.0 - self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - 16.0 * 2.0), height: 40.0)) + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - 16.0 * 2.0 - 32.0), height: 40.0)) self.textNode.isUserInteractionEnabled = isEnabled self.textNode.alpha = isEnabled ? 1.0 : 0.6 @@ -1103,6 +1181,8 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT private let textNode: EditableTextNode private let textNodeContainer: ASDisplayNode private let measureTextNode: ImmediateTextNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode private let topSeparator: ASDisplayNode private let requestUpdateHeight: () -> Void @@ -1124,11 +1204,45 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT self.measureTextNode.maximumNumberOfLines = 0 self.topSeparator = ASDisplayNode() + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + super.init() self.textNodeContainer.addSubnode(self.textNode) self.addSubnode(self.textNodeContainer) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) self.addSubnode(self.topSeparator) + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + guard let theme = self.theme else { + return + } + let attributedText = NSAttributedString(string: "", font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor) + self.textNode.attributedText = attributedText + self.requestUpdateHeight() + self.updateClearButtonVisibility() } func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { @@ -1142,6 +1256,8 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT self.textNode.clipsToBounds = true self.textNode.delegate = self self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) } self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor @@ -1163,18 +1279,39 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT } let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) self.measureTextNode.attributedText = attributedMeasureText - let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16 * 2.0, height: .greatestFiniteMagnitude)) + let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) self.currentMeasuredHeight = measureTextSize.height let height = measureTextSize.height + 22.0 - let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0, height: max(height, 1000.0))) + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0))) self.textNodeContainer.frame = textNodeFrame self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size) return height } + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden + } + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { guard let theme = self.theme else { return true @@ -1239,6 +1376,10 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { return self.itemNodes[key]?.text } + func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) { + self.itemNodes[key]?.layer.addShakeAnimation() + } + func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, cachedData: CachedPeerData?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { let avatarSize: CGFloat = 100.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize)) @@ -1371,7 +1512,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? - var requestAvatarExpansion: (([AvatarGalleryEntry], (ASDisplayNode, CGRect, () -> (UIView?, UIView?))) -> Void)? + var requestAvatarExpansion: (([AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)? var requestOpenAvatarForEditing: (() -> Void)? var requestUpdateLayout: (() -> Void)? @@ -1441,13 +1582,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.addSubnode(self.navigationButtonContainer) self.avatarListNode.avatarContainerNode.tapped = { [weak self] in - guard let strongSelf = self else { - return - } - let avatarNode = strongSelf.avatarListNode.avatarContainerNode.avatarNode - strongSelf.requestAvatarExpansion?(strongSelf.avatarListNode.listContainerNode.galleryEntries, (avatarNode, avatarNode.bounds, { [weak avatarNode] in - return (avatarNode?.view.snapshotContentTree(unhide: true), nil) - })) + self?.initiateAvatarExpansion() } self.editingContentNode.avatarNode.tapped = { [weak self] in guard let strongSelf = self else { @@ -1457,8 +1592,51 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - func updateAvatarIsHidden(_ isHidden: Bool) { - self.avatarListNode.avatarContainerNode.avatarNode.isHidden = isHidden + func initiateAvatarExpansion() { + if self.isAvatarExpanded { + if let currentEntry = self.avatarListNode.listContainerNode.currentEntry { + self.requestAvatarExpansion?(self.avatarListNode.listContainerNode.galleryEntries, self.avatarListNode.listContainerNode.currentEntry, self.avatarTransitionArguments(entry: currentEntry)) + } + } else if let entry = self.avatarListNode.listContainerNode.galleryEntries.first{ + let avatarNode = self.avatarListNode.avatarContainerNode.avatarNode + self.requestAvatarExpansion?(self.avatarListNode.listContainerNode.galleryEntries, nil, self.avatarTransitionArguments(entry: entry)) + } + } + + func avatarTransitionArguments(entry: AvatarGalleryEntry) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + if self.isAvatarExpanded { + if let avatarNode = self.avatarListNode.listContainerNode.currentItemNode?.imageNode { + return (avatarNode, avatarNode.bounds, { [weak avatarNode] in + return (avatarNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else { + return nil + } + } else if entry == self.avatarListNode.listContainerNode.galleryEntries.first { + let avatarNode = self.avatarListNode.avatarContainerNode.avatarNode + return (avatarNode, avatarNode.bounds, { [weak avatarNode] in + return (avatarNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else { + return nil + } + } + + func addToAvatarTransitionSurface(view: UIView) { + if self.isAvatarExpanded { + self.avatarListNode.listContainerNode.view.addSubview(view) + } else { + self.view.addSubview(view) + } + } + + func updateAvatarIsHidden(entry: AvatarGalleryEntry?) { + if let entry = entry { + self.avatarListNode.avatarContainerNode.avatarNode.isHidden = entry == self.avatarListNode.listContainerNode.galleryEntries.first + } else { + self.avatarListNode.avatarContainerNode.avatarNode.isHidden = false + } + self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry) } func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isContact: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift index a3855453af..c7eba4e7ac 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift @@ -26,6 +26,7 @@ protocol PeerInfoPaneNode: ASDisplayNode { final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey let node: PeerInfoPaneNode + var isAnimatingOut: Bool = false private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)? init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { @@ -114,6 +115,10 @@ struct PeerInfoPaneSpecifier: Equatable { var title: String } +private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { + return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) +} + final class PeerInfoPaneTabsContainerNode: ASDisplayNode { private let scrollNode: ASScrollNode private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] @@ -148,7 +153,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { self.scrollNode.addSubnode(self.selectedLineNode) } - func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) let focusOnSelectedPane = self.currentParams?.1 != selectedPane @@ -192,8 +197,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode, Bool)] = [] var totalRawTabSize: CGFloat = 0.0 + var selectionFrames: [CGRect] = [] - var selectedFrame: CGRect? for specifier in paneList { guard let paneNode = self.paneNodes[specifier.key] else { continue @@ -208,8 +213,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { totalRawTabSize += paneNodeSize.width } - let spacing: CGFloat = 32.0 - if tabSizes.count == 1 { + let minSpacing: CGFloat = 10.0 + if tabSizes.count <= 1 { for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] let leftOffset: CGFloat = 16.0 @@ -226,36 +231,63 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) - if paneList[i].key == selectedPane { - selectedFrame = paneFrame - } + selectionFrames.append(paneFrame) } self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) - } else if totalRawTabSize + CGFloat(tabSizes.count + 1) * spacing <= size.width { + } else if totalRawTabSize + CGFloat(tabSizes.count + 1) * minSpacing <= size.width { let availableSpace = size.width let availableSpacing = availableSpace - totalRawTabSize let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1)) - var leftOffset = perTabSpacing - for i in 0 ..< tabSizes.count { - let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] - - let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) - if wasAdded { - paneNode.frame = paneFrame - paneNode.alpha = 0.0 - transition.updateAlpha(node: paneNode, alpha: 1.0) - } else { - transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + let normalizedPerTabWidth = floor(availableSpace / CGFloat(tabSizes.count)) + var maxSpacing: CGFloat = 0.0 + var minSpacing: CGFloat = .greatestFiniteMagnitude + for i in 0 ..< tabSizes.count - 1 { + let distanceToNextBoundary = (normalizedPerTabWidth - tabSizes[i].0.width) / 2.0 + let nextDistanceToBoundary = (normalizedPerTabWidth - tabSizes[i + 1].0.width) / 2.0 + let distance = nextDistanceToBoundary + distanceToNextBoundary + maxSpacing = max(distance, maxSpacing) + minSpacing = min(distance, minSpacing) + } + + if minSpacing >= 100.0 || (maxSpacing / minSpacing) < 0.2 { + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + + let paneFrame = CGRect(origin: CGPoint(x: CGFloat(i) * normalizedPerTabWidth + floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0), y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + let areaSideInset = floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + selectionFrames.append(paneFrame) } - let areaSideInset = floor(perTabSpacing / 2.0) - paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) - paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) - - leftOffset += paneNodeSize.width + perTabSpacing - - if paneList[i].key == selectedPane { - selectedFrame = paneFrame + } else { + var leftOffset = perTabSpacing + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + let areaSideInset = floor(perTabSpacing / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + leftOffset += paneNodeSize.width + perTabSpacing + + selectionFrames.append(paneFrame) } } self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) @@ -272,14 +304,29 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { } else { transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) } - paneNode.updateArea(size: paneFrame.size, sideInset: spacing) - paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing, bottom: 0.0, right: -spacing) - if paneList[i].key == selectedPane { - selectedFrame = paneFrame - } - leftOffset += paneNodeSize.width + spacing + paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing, bottom: 0.0, right: -minSpacing) + + selectionFrames.append(paneFrame) + + leftOffset += paneNodeSize.width + minSpacing + } + self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset, height: size.height) + } + + var selectedFrame: CGRect? + if let selectedPane = selectedPane, let currentIndex = paneList.index(where: { $0.key == selectedPane }) { + if currentIndex != 0 && transitionFraction > 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex - 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else if currentIndex != paneList.count - 1 && transitionFraction < 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex + 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else { + selectedFrame = selectionFrames[currentIndex] } - self.scrollNode.view.contentSize = CGSize(width: leftOffset - spacing + sideInset, height: size.height) } if let selectedFrame = selectedFrame { @@ -313,7 +360,60 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { } } -final class PeerInfoPaneContainerNode: ASDisplayNode { +private final class PeerInfoPendingPane { + let pane: PeerInfoPaneWrapper + private var disposable: Disposable? + var isReady: Bool = false + + init( + context: AccountContext, + chatControllerInteraction: ChatControllerInteraction, + data: PeerInfoScreenData, + openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, + requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, + peerId: PeerId, + key: PeerInfoPaneKey, + hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void + ) { + let paneNode: PeerInfoPaneNode + switch key { + case .media: + paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId) + case .files: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) + case .links: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage) + case .voice: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo) + case .music: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music) + case .groupsInCommon: + paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!) + case .members: + if case let .longList(membersContext) = data.members { + paneNode = PeerInfoMembersPaneNode(context: context, peerId: peerId, membersContext: membersContext, action: { member, action in + requestPerformPeerMemberAction(member, action) + }) + } else { + preconditionFailure() + } + } + + self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) + self.disposable = (paneNode.isReady + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.isReady = true + hasBecomeReady(key) + }) + } + + deinit { + self.disposable?.dispose() + } +} + +final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private let peerId: PeerId @@ -326,11 +426,22 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { var didSetIsReady = false private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? - private(set) var currentPaneKey: PeerInfoPaneKey? - private(set) var currentPane: PeerInfoPaneWrapper? - private var currentCandidatePaneKey: PeerInfoPaneKey? - private var candidatePane: (PeerInfoPaneWrapper, Disposable, Bool)? + private(set) var currentPaneKey: PeerInfoPaneKey? + var pendingSwitchToPaneKey: PeerInfoPaneKey? + + var currentPane: PeerInfoPaneWrapper? { + if let currentPaneKey = self.currentPaneKey { + return self.currentPanes[currentPaneKey] + } else { + return nil + } + } + + private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] + private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] + + private var transitionFraction: CGFloat = 0.0 var selectionPanelNode: PeerInfoSelectionPanelNode? @@ -376,14 +487,95 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { } return } - if strongSelf.currentCandidatePaneKey == key { - return + if strongSelf.currentPanes[key] != nil { + strongSelf.currentPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + } + } else if strongSelf.pendingSwitchToPaneKey != key { + strongSelf.pendingSwitchToPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + } } - strongSelf.currentCandidatePaneKey = key - - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + } + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), enableBothDirections: true, canBegin: { [weak self] in + guard let strongSelf = self else { + return false } + return strongSelf.currentPanes.count > 1 + }) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(panRecognizer) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + var transitionFraction = translation.x / size.width + if currentIndex <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if currentIndex >= availablePanes.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + } + case .cancelled, .ended: + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + var directionIsToRight: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + if abs(translation.x) > size.width / 2.0 { + directionIsToRight = translation.x > size.width / 2.0 + } + } + if let directionIsToRight = directionIsToRight { + var updatedIndex = currentIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, availablePanes.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + let switchToKey = availablePanes[updatedIndex] + if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ + self.currentPaneKey = switchToKey + } + } + self.transitionFraction = 0.0 + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) + } + default: + break } } @@ -408,8 +600,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { } func updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { - self.currentPane?.node.updateSelectedMessages(animated: animated) - self.candidatePane?.0.node.updateSelectedMessages(animated: animated) + for (_, pane) in self.currentPanes { + pane.node.updateSelectedMessages(animated: animated) + } + for (_, pane) in self.pendingPanes { + pane.pane.node.updateSelectedMessages(animated: animated) + } } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { @@ -417,6 +613,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { let availablePanes = data?.availablePanes ?? [] self.currentAvailablePanes = availablePanes + let previousCurrentPaneKey = self.currentPaneKey + if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { var nextCandidatePaneKey: PeerInfoPaneKey? if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 { @@ -431,25 +629,21 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { } if let nextCandidatePaneKey = nextCandidatePaneKey { - if self.currentCandidatePaneKey != nextCandidatePaneKey { - self.currentCandidatePaneKey = nextCandidatePaneKey - } + self.pendingSwitchToPaneKey = nextCandidatePaneKey } else { - self.currentCandidatePaneKey = nil - if let (_, disposable, _) = self.candidatePane { - disposable.dispose() - self.candidatePane = nil - } - if let currentPane = self.currentPane { - self.currentPane = nil - currentPane.node.removeFromSupernode() - } + self.currentPaneKey = nil + self.pendingSwitchToPaneKey = nil } } else if self.currentPaneKey == nil { - self.currentCandidatePaneKey = availablePanes.first + self.pendingSwitchToPaneKey = availablePanes.first } - let previousCurrentPaneKey = self.currentPaneKey + let currentIndex: Int? + if let currentPaneKey = self.currentPaneKey { + currentIndex = availablePanes.index(of: currentPaneKey) + } else { + currentIndex = nil + } self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) @@ -469,100 +663,159 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) - if let currentCandidatePaneKey = self.currentCandidatePaneKey { - if self.candidatePane?.0.key != currentCandidatePaneKey { - self.candidatePane?.1.dispose() - - let paneNode: PeerInfoPaneNode - switch currentCandidatePaneKey { - case .media: - paneNode = PeerInfoVisualMediaPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId) - case .files: - paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .file) - case .links: - paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .webPage) - case .voice: - paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .voiceOrInstantVideo) - case .music: - paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .music) - case .groupsInCommon: - paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: self.peerId, chatControllerInteraction: self.chatControllerInteraction!, openPeerContextAction: self.openPeerContextAction!, groupsInCommonContext: data!.groupsInCommon!) - case .members: - if case let .longList(membersContext) = data?.members { - paneNode = PeerInfoMembersPaneNode(context: self.context, peerId: self.peerId, membersContext: membersContext, action: { [weak self] member, action in - self?.requestPerformPeerMemberAction?(member, action) - }) - } else { - preconditionFailure() - } + var visiblePaneIndices: [Int] = [] + var requiredPendingKeys: [PeerInfoPaneKey] = [] + if let currentIndex = currentIndex { + if currentIndex != 0 { + visiblePaneIndices.append(currentIndex - 1) + } + visiblePaneIndices.append(currentIndex) + if currentIndex != availablePanes.count - 1 { + visiblePaneIndices.append(currentIndex + 1) + } + + for index in visiblePaneIndices { + let indexOffset = CGFloat(index - currentIndex) + let key = availablePanes[index] + if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { + requiredPendingKeys.append(key) + } + } + } + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey { + if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { + if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { + requiredPendingKeys.append(pendingSwitchToPaneKey) } - - let disposable = MetaDisposable() - self.candidatePane = (PeerInfoPaneWrapper(key: currentCandidatePaneKey, node: paneNode), disposable, false) - - var shouldReLayout = false - disposable.set((paneNode.isReady - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - if let (candidatePane, disposable, _) = strongSelf.candidatePane { - strongSelf.candidatePane = (candidatePane, disposable, true) - - if shouldReLayout { - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: strongSelf.currentPane != nil ? .animated(duration: 0.35, curve: .spring) : .immediate) - } - } - } - })) - shouldReLayout = true } } - if let (candidatePane, _, isReady) = self.candidatePane, isReady { - let previousPane = self.currentPane - self.candidatePane = nil - self.currentPaneKey = candidatePane.key - self.currentCandidatePaneKey = nil - self.currentPane = candidatePane - - if let selectionPanelNode = self.selectionPanelNode { - self.insertSubnode(candidatePane.node, belowSubnode: selectionPanelNode) - } else { - self.addSubnode(candidatePane.node) + for key in requiredPendingKeys { + if self.pendingPanes[key] == nil { + var leftScope = false + let pane = PeerInfoPendingPane( + context: self.context, + chatControllerInteraction: self.chatControllerInteraction!, + data: data!, + openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction?(peer, node, gesture) + }, + requestPerformPeerMemberAction: { [weak self] member, action in + self?.requestPerformPeerMemberAction?(member, action) + }, + peerId: self.peerId, + key: key, + hasBecomeReady: { [weak self] key in + let apply: () -> Void = { + guard let strongSelf = self else { + return + } + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + var transition: ContainedViewLayoutTransition = .immediate + if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { + transition = .animated(duration: 0.4, curve: .spring) + } + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) + } + } + if leftScope { + apply() + } + } + ) + self.pendingPanes[key] = pane + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) + leftScope = true } - candidatePane.node.frame = paneFrame - candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: max(0.0, visibleHeight - paneFrame.minY), isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) - - if let previousPane = previousPane { - let directionToRight: Bool - if let previousIndex = availablePanes.index(of: previousPane.key), let updatedIndex = availablePanes.index(of: candidatePane.key) { - directionToRight = previousIndex < updatedIndex - } else { - directionToRight = false - } - - let offset: CGFloat = directionToRight ? previousPane.node.bounds.width : -previousPane.node.bounds.width - - transition.animatePositionAdditive(node: candidatePane.node, offset: CGPoint(x: offset, y: 0.0)) - let previousNode = previousPane.node - transition.updateFrame(node: previousNode, frame: paneFrame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - } - } else if let currentPane = self.currentPane { - let paneWasAdded = currentPane.node.supernode == nil - if paneWasAdded { - self.addSubnode(currentPane.node) - } - - let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : transition - paneTransition.updateFrame(node: currentPane.node, frame: paneFrame) - currentPane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } + for (key, pane) in self.pendingPanes { + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) + + if pane.isReady { + self.pendingPanes.removeValue(forKey: key) + self.currentPanes[key] = pane.pane + } + } + + var paneDefaultTransition = transition + var previousPaneKey: PeerInfoPaneKey? + var paneSwitchAnimationOffset: CGFloat = 0.0 + + var updatedCurrentIndex = currentIndex + var animatePaneTransitionOffset: CGFloat? + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let pane = self.currentPanes[pendingSwitchToPaneKey] { + self.pendingSwitchToPaneKey = nil + previousPaneKey = self.currentPaneKey + self.currentPaneKey = pendingSwitchToPaneKey + updatedCurrentIndex = availablePanes.index(of: pendingSwitchToPaneKey) + if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.index(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { + if updatedCurrentIndex < previousIndex { + paneSwitchAnimationOffset = -size.width + } else { + paneSwitchAnimationOffset = size.width + } + } + + paneDefaultTransition = .immediate + } + + for (key, pane) in self.currentPanes { + if let index = availablePanes.index(of: key), let updatedCurrentIndex = updatedCurrentIndex { + var paneWasAdded = false + if pane.node.supernode == nil { + self.addSubnode(pane.node) + paneWasAdded = true + } + let indexOffset = CGFloat(index - updatedCurrentIndex) + + let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition + let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0) + + let paneCompletion: () -> Void = { [weak self, weak pane] in + guard let strongSelf = self, let pane = pane else { + return + } + pane.isAnimatingOut = false + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey), let paneIndex = availablePanes.index(of: key), abs(paneIndex - currentIndex) <= 1 { + } else { + if let pane = strongSelf.currentPanes.removeValue(forKey: key) { + //print("remove \(key)") + pane.node.removeFromSupernode() + } + } + } + } + if let previousPaneKey = previousPaneKey, key == previousPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } else if let previousPaneKey = previousPaneKey, key == self.currentPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : { + paneCompletion() + }) + } else { + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } + pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + } + } + + //print("currentPanes: \(self.currentPanes.map { $0.0 })") + transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight))) self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String @@ -583,18 +836,18 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { title = presentationData.strings.PeerInfo_PaneMembers } return PeerInfoPaneSpecifier(key: key, title: title) - }, selectedPane: self.currentPaneKey, transition: transition) + }, selectedPane: self.currentPaneKey, transitionFraction: self.transitionFraction, transition: transition) - if let (candidatePane, _, _) = self.candidatePane { + for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate - paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame) - candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) + paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) } if !self.didSetIsReady && data != nil { - if let currentPane = self.currentPane { + if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { self.didSetIsReady = true self.isReady.set(currentPane.node.isReady) - } else if self.candidatePane == nil { + } else if self.pendingSwitchToPaneKey == nil { self.didSetIsReady = true self.isReady.set(.single(true)) } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift index caf4386b55..706fe867f4 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift @@ -492,6 +492,7 @@ private final class PeerInfoInteraction { let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void + let requestLayout: () -> Void init( openUsername: @escaping (String) -> Void, @@ -519,7 +520,8 @@ private final class PeerInfoInteraction { openPeerInfo: @escaping (Peer) -> Void, performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void, - performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void + performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, + requestLayout: @escaping () -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -547,6 +549,7 @@ private final class PeerInfoInteraction { self.performMemberAction = performMemberAction self.openPeerInfoContextMenu = openPeerInfoContextMenu self.performBioLinkAction = performBioLinkAction + self.requestLayout = requestLayout } } @@ -582,6 +585,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openPhone(phone) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode) + }, requestLayout: { + interaction.requestLayout() })) } if let username = user.username { @@ -589,13 +594,19 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openUsername(username) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link, sourceNode) + }, requestLayout: { + interaction.requestLayout() })) } if let cachedData = data.cachedData as? CachedUserData { if user.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledBioEntities : []), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledBioEntities : []), action: nil, requestLayout: { + interaction.requestLayout() + })) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: []), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) } } if nearbyPeer { @@ -609,7 +620,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { if !data.isContact { if user.botInfo == nil { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_AddContact, action: { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.Conversation_AddToContacts, action: { interaction.openAddContact() })) } @@ -666,13 +677,19 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openUsername(username) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link, sourceNode) + }, requestLayout: { + interaction.requestLayout() })) } if let cachedData = data.cachedData as? CachedChannelData { if channel.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: { + interaction.requestLayout() + })) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) } if case .broadcast = channel.info { @@ -702,9 +719,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else if let group = data.peer as? TelegramGroup { if let cachedData = data.cachedData as? CachedGroupData { if group.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: { + interaction.requestLayout() + })) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) } } } @@ -807,7 +828,9 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: linkText, text: presentationData.strings.Channel_TypeSetup_Title, action: { interaction.editingOpenPublicLinkSetup() })) - + } + + if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { let discussionGroupTitle: String if let cachedData = data.cachedData as? CachedChannelData { if let peer = data.linkedDiscussionPeer { @@ -1046,6 +1069,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private let updateAvatarDisposable = MetaDisposable() private let currentAvatarMixin = Atomic(value: nil) + private var groupMembersSearchContext: GroupMembersSearchContext? + private let _ready = Promise() var ready: Promise { return self._ready @@ -1145,6 +1170,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, performBioLinkAction: { [weak self] action, item in self?.performBioLinkAction(action: action, item: item) + }, + requestLayout: { [weak self] in + self?.requestLayout() } ) @@ -1595,25 +1623,26 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.performButtonAction(key: key) } - self.headerNode.requestAvatarExpansion = { [weak self] entries, transitionNode in + self.headerNode.requestAvatarExpansion = { [weak self] entries, centralEntry, _ in guard let strongSelf = self, let peer = strongSelf.data?.peer, peer.smallProfileImage != nil else { return } let entriesPromise = Promise<[AvatarGalleryEntry]>(entries) - let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: entriesPromise, replaceRootController: { controller, ready in + let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, sourceHasRoundCorners: !strongSelf.headerNode.isAvatarExpanded, remoteEntries: entriesPromise, centralEntryIndex: centralEntry.flatMap { entries.index(of: $0) }, replaceRootController: { controller, ready in }) strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in - if entry == entries.first { - self?.headerNode.updateAvatarIsHidden(true) - } else { - self?.headerNode.updateAvatarIsHidden(false) - } + self?.headerNode.updateAvatarIsHidden(entry: entry) })) strongSelf.view.endEditing(true) - strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { _ in - return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ in - }) + strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in + if let transitionNode = self?.headerNode.avatarTransitionArguments(entry: entry) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in + self?.headerNode.addToAvatarTransitionSurface(view: view) + }) + } else { + return nil + } })) } @@ -1660,39 +1689,60 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? "" if peer.firstName != firstName || peer.lastName != lastName { - strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName) - |> deliverOnMainQueue).start(error: { _ in - guard let strongSelf = self else { - return + if firstName.isEmpty && lastName.isEmpty { + if strongSelf.hapticFeedback == nil { + strongSelf.hapticFeedback = HapticFeedback() } - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - }, completed: { - guard let strongSelf = self else { - return + strongSelf.hapticFeedback?.error() + strongSelf.headerNode.editingContentNode.shakeTextForKey(.firstName) + } else { + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() } - let context = strongSelf.context - let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: peer.id) - |> mapToSignal { peer, _ -> Signal in - guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else { - return .complete() + strongSelf.controller?.present(statusController, in: .window(.root)) + strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return } - return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([])) - |> take(1) - |> mapToSignal { records -> Signal in - var signals: [Signal] = [] - if let contactDataManager = context.sharedContext.contactDataManager { - for (id, basicData) in records { - signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: ""), to: id)) - } - } - return combineLatest(signals) - |> mapToSignal { _ -> Signal in + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: peer.id) + |> mapToSignal { peer, _ -> Signal in + guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else { return .complete() } - } - }).start() - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - })) + return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([])) + |> take(1) + |> mapToSignal { records -> Signal in + var signals: [Signal] = [] + if let contactDataManager = context.sharedContext.contactDataManager { + for (id, basicData) in records { + signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: ""), to: id)) + } + } + return combineLatest(signals) + |> mapToSignal { _ -> Signal in + return .complete() + } + } + }).start() + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } } else { strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) } @@ -1703,66 +1753,108 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" - var updateDataSignals: [Signal] = [] - - if title != group.title { - updateDataSignals.append( - updatePeerTitle(account: strongSelf.context.account, peerId: group.id, title: title) - |> ignoreValues - |> mapError { _ in return Void() } - ) - } - if description != (data.cachedData as? CachedGroupData)?.about { - updateDataSignals.append( - updatePeerDescription(account: strongSelf.context.account, peerId: group.id, description: description.isEmpty ? nil : description) - |> ignoreValues - |> mapError { _ in return Void() } - ) - } - strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) - |> deliverOnMainQueue).start(error: { _ in - guard let strongSelf = self else { - return + if title.isEmpty { + if strongSelf.hapticFeedback == nil { + strongSelf.hapticFeedback = HapticFeedback() } - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - }, completed: { - guard let strongSelf = self else { - return + strongSelf.hapticFeedback?.error() + + strongSelf.headerNode.editingContentNode.shakeTextForKey(.title) + } else { + var updateDataSignals: [Signal] = [] + + if title != group.title { + updateDataSignals.append( + updatePeerTitle(account: strongSelf.context.account, peerId: group.id, title: title) + |> ignoreValues + |> mapError { _ in return Void() } + ) } - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - })) + if description != (data.cachedData as? CachedGroupData)?.about { + updateDataSignals.append( + updatePeerDescription(account: strongSelf.context.account, peerId: group.id, description: description.isEmpty ? nil : description) + |> ignoreValues + |> mapError { _ in return Void() } + ) + } + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() + } + strongSelf.controller?.present(statusController, in: .window(.root)) + + strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } } else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(peer: channel) { let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" - var updateDataSignals: [Signal] = [] - - if title != channel.title { - updateDataSignals.append( - updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title) - |> ignoreValues - |> mapError { _ in return Void() } - ) - } - if description != (data.cachedData as? CachedChannelData)?.about { - updateDataSignals.append( - updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description) - |> ignoreValues - |> mapError { _ in return Void() } - ) - } - strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) - |> deliverOnMainQueue).start(error: { _ in - guard let strongSelf = self else { - return + if title.isEmpty { + strongSelf.headerNode.editingContentNode.shakeTextForKey(.title) + } else { + var updateDataSignals: [Signal] = [] + + if title != channel.title { + updateDataSignals.append( + updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title) + |> ignoreValues + |> mapError { _ in return Void() } + ) } - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - }, completed: { - guard let strongSelf = self else { - return + if description != (data.cachedData as? CachedChannelData)?.about { + updateDataSignals.append( + updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description) + |> ignoreValues + |> mapError { _ in return Void() } + ) } - strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) - })) + + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() + } + strongSelf.controller?.present(statusController, in: .window(.root)) + + strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } } else { strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) } @@ -1821,6 +1913,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private func updateData(_ data: PeerInfoScreenData) { + let previousData = self.data var previousMemberCount: Int? if let data = self.data { if let members = data.members, case let .shortList(_, memberList) = members { @@ -1828,6 +1921,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } self.data = data + if previousData?.members?.membersContext !== data.members?.membersContext { + if let peer = data.peer, let _ = data.members { + self.groupMembersSearchContext = GroupMembersSearchContext(context: self.context, peerId: peer.id) + } else { + self.groupMembersSearchContext = nil + } + } if let (layout, navigationHeight) = self.validLayout { var updatedMemberCount: Int? if let data = self.data { @@ -2055,6 +2155,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } + if user.botInfo == nil && data.isContact { + items.append(ActionSheetButtonItem(title: presentationData.strings.Profile_ShareContactButton, color: .accent, action: { [weak self] in + dismissAction() + guard let strongSelf = self else { + return + } + if let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone { + let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) + let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact))) + strongSelf.controller?.present(shareController, in: .window(.root)) + } + })) + } + if user.botInfo == nil && !user.flags.contains(.isSupport) { items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in dismissAction() @@ -2794,6 +2908,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.context.sharedContext.handleTextLinkAction(context: self.context, peerId: peer.id, navigateDisposable: self.resolveUrlDisposable, controller: controller, action: action, itemLink: item) } + private func requestLayout() { + self.headerNode.requestUpdateLayout?() + } + private func openDeletePeer() { let peerId = self.peerId let _ = (self.context.account.postbox.transaction { transaction -> Peer? in @@ -3423,7 +3541,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey { - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChannelMembersSearchContainerNode(context: self.context, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: nil, openPeer: { [weak self] peer, participant in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChannelMembersSearchContainerNode(context: self.context, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in self?.openPeer(peerId: peer.id, navigation: .info) }, updateActivity: { _ in }, pushController: { [weak self] c in @@ -3798,12 +3916,15 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.canAddVelocity = true + self.canOpenAvatarByDragging = self.headerNode.isAvatarExpanded } private var previousVelocityM1: CGFloat = 0.0 private var previousVelocity: CGFloat = 0.0 private var canAddVelocity: Bool = false + private var canOpenAvatarByDragging = false + private let velocityKey: String = encodeText("`wfsujdbmWfmpdjuz", -1) func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -3825,9 +3946,15 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if offsetY <= -32.0 && scrollView.isDragging && scrollView.isTracking { if let peer = self.data?.peer, peer.smallProfileImage != nil { shouldBeExpanded = true + + if self.canOpenAvatarByDragging && self.headerNode.isAvatarExpanded && offsetY <= -32.0 { + self.canOpenAvatarByDragging = false + self.headerNode.initiateAvatarExpansion() + } } } else if offsetY >= 1.0 { shouldBeExpanded = false + self.canOpenAvatarByDragging = false } if let shouldBeExpanded = shouldBeExpanded, shouldBeExpanded != self.headerNode.isAvatarExpanded { let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift index c180b8a1ef..2865a2719a 100644 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift @@ -684,7 +684,7 @@ class WebSearchControllerNode: ASDisplayNode { var entries: [WebSearchGalleryEntry] = [] var centralIndex: Int = 0 for i in 0 ..< results.count { - entries.append(WebSearchGalleryEntry(result: results[i])) + entries.append(WebSearchGalleryEntry(index: entries.count, result: results[i])) if results[i] == currentResult { centralIndex = i } diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index 38022196fa..44d6053249 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -28,6 +28,7 @@ final class WebSearchGalleryControllerInteraction { } struct WebSearchGalleryEntry: Equatable { + let index: Int let result: ChatContextResult static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool { @@ -39,11 +40,11 @@ struct WebSearchGalleryEntry: Equatable { case let .externalReference(_, _, type, _, _, _, content, thumbnail, _): if let content = content, type == "gif", let thumbnailResource = thumbnail?.resource, let dimensions = content.dimensions { let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } case let .internalReference(_, _, _, _, _, _, file, _): if let file = file { - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } } preconditionFailure() diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index 1f69d4ec7a..788478b84f 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -14,15 +14,22 @@ import TelegramUniversalVideoContent import GalleryUI class WebSearchVideoGalleryItem: GalleryItem { + var id: AnyHashable { + return self.index + } + + let index: Int + let context: AccountContext let presentationData: PresentationData let result: ChatContextResult let content: UniversalVideoContent let controllerInteraction: WebSearchGalleryControllerInteraction? - init(context: AccountContext, presentationData: PresentationData, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { + init(context: AccountContext, presentationData: PresentationData, index: Int, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { self.context = context self.presentationData = presentationData + self.index = index self.result = result self.content = content self.controllerInteraction = controllerInteraction