Post-release bug fixes

This commit is contained in:
Ali 2020-02-14 14:24:29 +01:00
parent 267bac83be
commit 0b6663fad5
37 changed files with 1318 additions and 390 deletions

View File

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

View File

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

View File

@ -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<UITouch>, with event: UIEvent) {
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if !self.canBegin() {
self.state = .failed
return
@ -68,17 +71,17 @@ class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
override public func touchesMoved(_ touches: Set<UITouch>, 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

View File

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

View File

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

View File

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

View File

@ -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<Bool>?) -> 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<GalleryMessageHistoryView?, NoError> 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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Bool>?) -> Void
public init(context: AccountContext, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, synchronousLoad: Bool = false) {
public init(context: AccountContext, peer: Peer, sourceHasRoundCorners: Bool = true, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> 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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TGMediaAvatarMenuMixin?>(value: nil)
private var groupMembersSearchContext: GroupMembersSearchContext?
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
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<Void, NoError> 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<Void, NoError> in
var signals: [Signal<DeviceContactExtendedData?, NoError>] = []
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<Void, NoError> 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<Void, NoError> 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<Void, NoError> in
var signals: [Signal<DeviceContactExtendedData?, NoError>] = []
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<Void, NoError> 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<Never, Void>] = []
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<Never, Void>] = []
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<Never, Void>] = []
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<Never, Void>] = []
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)

View File

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

View File

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

View File

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