mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Post-release bug fixes
This commit is contained in:
parent
267bac83be
commit
0b6663fad5
@ -14,6 +14,10 @@ public final class ContextControllerSourceNode: ASDisplayNode {
|
|||||||
public var shouldBegin: ((CGPoint) -> Bool)?
|
public var shouldBegin: ((CGPoint) -> Bool)?
|
||||||
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
|
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
|
||||||
|
|
||||||
|
public func cancelGesture() {
|
||||||
|
self.contextGesture?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override public func didLoad() {
|
override public func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ public class ImmediateTextNode: TextNode {
|
|||||||
public var insets: UIEdgeInsets = UIEdgeInsets()
|
public var insets: UIEdgeInsets = UIEdgeInsets()
|
||||||
public var textShadowColor: UIColor?
|
public var textShadowColor: UIColor?
|
||||||
public var textStroke: (UIColor, CGFloat)?
|
public var textStroke: (UIColor, CGFloat)?
|
||||||
|
public var cutout: TextNodeCutout?
|
||||||
|
|
||||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||||
private var linkHighlightingNode: LinkHighlightingNode?
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
@ -57,7 +58,7 @@ public class ImmediateTextNode: TextNode {
|
|||||||
|
|
||||||
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||||
let makeLayout = TextNode.asyncLayout(self)
|
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()
|
let _ = apply()
|
||||||
if layout.numberOfLines > 1 {
|
if layout.numberOfLines > 1 {
|
||||||
self.trailingLineWidth = layout.trailingLineWidth
|
self.trailingLineWidth = layout.trailingLineWidth
|
||||||
@ -69,7 +70,7 @@ public class ImmediateTextNode: TextNode {
|
|||||||
|
|
||||||
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
|
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
|
||||||
let makeLayout = TextNode.asyncLayout(self)
|
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()
|
let _ = apply()
|
||||||
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
|
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,15 @@ private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||||
var validatedGesture = false
|
private let enableBothDirections: Bool
|
||||||
var firstLocation: CGPoint = CGPoint()
|
|
||||||
private let canBegin: () -> 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
|
self.canBegin = canBegin
|
||||||
|
|
||||||
super.init(target: target, action: action)
|
super.init(target: target, action: action)
|
||||||
@ -44,13 +47,13 @@ class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.maximumNumberOfTouches = 1
|
self.maximumNumberOfTouches = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override func reset() {
|
override public func reset() {
|
||||||
super.reset()
|
super.reset()
|
||||||
|
|
||||||
validatedGesture = false
|
validatedGesture = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
if !self.canBegin() {
|
if !self.canBegin() {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
return
|
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 location = touches.first!.location(in: self.view)
|
||||||
let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y)
|
let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y)
|
||||||
|
|
||||||
let absTranslationX: CGFloat = abs(translation.x)
|
let absTranslationX: CGFloat = abs(translation.x)
|
||||||
let absTranslationY: CGFloat = abs(translation.y)
|
let absTranslationY: CGFloat = abs(translation.y)
|
||||||
|
|
||||||
if !validatedGesture {
|
if !self.validatedGesture {
|
||||||
if self.firstLocation.x < 16.0 {
|
if !self.enableBothDirections && self.firstLocation.x < 16.0 {
|
||||||
validatedGesture = true
|
validatedGesture = true
|
||||||
} else if translation.x < 0.0 {
|
} else if !self.enableBothDirections && translation.x < 0.0 {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
|
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
|
@ -1190,14 +1190,14 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
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 {
|
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))
|
let effectiveBackButtonRect = CGRect(origin: CGPoint(), size: CGSize(width: self.backButtonNode.frame.maxX + 20.0, height: self.bounds.height))
|
||||||
if effectiveBackButtonRect.contains(point) {
|
if effectiveBackButtonRect.contains(point) {
|
||||||
return self.backButtonNode.internalHitTest(self.view.convert(point, to: self.backButtonNode.view), with: event)
|
return self.backButtonNode.internalHitTest(self.view.convert(point, to: self.backButtonNode.view), with: event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
guard let result = super.hitTest(point, with: event) else {
|
guard let result = super.hitTest(point, with: event) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -929,7 +929,12 @@ public class TextNode: ASDisplayNode {
|
|||||||
let coreTextLine: CTLine
|
let coreTextLine: CTLine
|
||||||
let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0)
|
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
|
coreTextLine = originalLine
|
||||||
} else {
|
} else {
|
||||||
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
|
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
|
||||||
@ -939,7 +944,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
|
let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
|
||||||
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
|
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
|
||||||
|
|
||||||
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken
|
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken
|
||||||
truncated = true
|
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)
|
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
|
||||||
layoutSize.height += fontLineHeight + fontLineSpacing
|
layoutSize.height += fontLineHeight + fontLineSpacing
|
||||||
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
|
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
|
||||||
@ -1032,7 +1037,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
if !lines.isEmpty && bottomCutoutEnabled {
|
if !lines.isEmpty && bottomCutoutEnabled {
|
||||||
let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width
|
let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width
|
||||||
if proposedWidth > layoutSize.width {
|
if proposedWidth > layoutSize.width {
|
||||||
if proposedWidth < constrainedSize.width {
|
if proposedWidth <= constrainedSize.width + .ulpOfOne {
|
||||||
layoutSize.width = proposedWidth
|
layoutSize.width = proposedWidth
|
||||||
} else {
|
} else {
|
||||||
layoutSize.height += bottomCutoutSize.height
|
layoutSize.height += bottomCutoutSize.height
|
||||||
|
@ -81,7 +81,7 @@ open class TransformImageNode: ASDisplayNode {
|
|||||||
let apply: () -> Void = {
|
let apply: () -> Void = {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if strongSelf.contents == nil {
|
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)
|
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
}
|
}
|
||||||
} else if strongSelf.contentAnimations.contains(.subsequentUpdates) {
|
} else if strongSelf.contentAnimations.contains(.subsequentUpdates) {
|
||||||
|
@ -253,6 +253,33 @@ private enum GalleryMessageHistoryView {
|
|||||||
return [entry]
|
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 {
|
public enum GalleryControllerItemSource {
|
||||||
@ -304,6 +331,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private let source: GalleryControllerItemSource
|
private let source: GalleryControllerItemSource
|
||||||
|
private let invertItemOrder: Bool
|
||||||
|
|
||||||
private let streamVideos: Bool
|
private let streamVideos: Bool
|
||||||
|
|
||||||
@ -324,6 +352,9 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
private let disposable = MetaDisposable()
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
private var entries: [MessageHistoryEntry] = []
|
private var entries: [MessageHistoryEntry] = []
|
||||||
|
private var hasLeftEntries: Bool = false
|
||||||
|
private var hasRightEntries: Bool = false
|
||||||
|
private var tagMask: MessageTags?
|
||||||
private var centralEntryStableId: UInt32?
|
private var centralEntryStableId: UInt32?
|
||||||
private var configuration: GalleryConfiguration?
|
private var configuration: GalleryConfiguration?
|
||||||
|
|
||||||
@ -346,9 +377,12 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
private var performAction: (GalleryControllerInteractionTapAction) -> Void
|
private var performAction: (GalleryControllerInteractionTapAction) -> Void
|
||||||
private var openActionOptions: (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) {
|
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.context = context
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.invertItemOrder = invertItemOrder
|
||||||
self.replaceRootController = replaceRootController
|
self.replaceRootController = replaceRootController
|
||||||
self.baseNavigationController = baseNavigationController
|
self.baseNavigationController = baseNavigationController
|
||||||
self.actionInteraction = actionInteraction
|
self.actionInteraction = actionInteraction
|
||||||
@ -444,13 +478,19 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.tagMask = view.tagMask
|
||||||
|
|
||||||
if invertItemOrder {
|
if invertItemOrder {
|
||||||
strongSelf.entries = entries.reversed()
|
strongSelf.entries = entries.reversed()
|
||||||
|
strongSelf.hasLeftEntries = view.hasLater
|
||||||
|
strongSelf.hasRightEntries = view.hasEarlier
|
||||||
if let centralEntryStableId = centralEntryStableId {
|
if let centralEntryStableId = centralEntryStableId {
|
||||||
strongSelf.centralEntryStableId = centralEntryStableId
|
strongSelf.centralEntryStableId = centralEntryStableId
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strongSelf.entries = entries
|
strongSelf.entries = entries
|
||||||
|
strongSelf.hasLeftEntries = view.hasEarlier
|
||||||
|
strongSelf.hasRightEntries = view.hasLater
|
||||||
strongSelf.centralEntryStableId = centralEntryStableId
|
strongSelf.centralEntryStableId = centralEntryStableId
|
||||||
}
|
}
|
||||||
if strongSelf.isViewLoaded {
|
if strongSelf.isViewLoaded {
|
||||||
@ -774,6 +814,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex {
|
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex {
|
||||||
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
|
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
|
||||||
}
|
}
|
||||||
|
self.updateVisibleDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func donePressed() {
|
@objc private func donePressed() {
|
||||||
@ -898,6 +939,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
var hiddenItem: (MessageId, Media)?
|
var hiddenItem: (MessageId, Media)?
|
||||||
if let index = index {
|
if let index = index {
|
||||||
let message = strongSelf.entries[index].message
|
let message = strongSelf.entries[index].message
|
||||||
|
strongSelf.centralEntryStableId = message.stableId
|
||||||
if let (media, _) = mediaForMessage(message: message) {
|
if let (media, _) = mediaForMessage(message: message) {
|
||||||
hiddenItem = (message.id, media)
|
hiddenItem = (message.id, media)
|
||||||
}
|
}
|
||||||
@ -910,6 +952,69 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
|
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
|
||||||
strongSelf.centralItemFooterContentNode.set(node.footerContent())
|
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 {
|
if strongSelf.didSetReady {
|
||||||
strongSelf._hiddenMedia.set(.single(hiddenItem))
|
strongSelf._hiddenMedia.set(.single(hiddenItem))
|
||||||
|
@ -21,6 +21,8 @@ public struct GalleryItemIndexData: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public protocol GalleryItem {
|
public protocol GalleryItem {
|
||||||
|
var id: AnyHashable { get }
|
||||||
|
|
||||||
func node() -> GalleryItemNode
|
func node() -> GalleryItemNode
|
||||||
func updateNode(node: GalleryItemNode)
|
func updateNode(node: GalleryItemNode)
|
||||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)?
|
func thumbnailItem() -> (Int64, GalleryThumbnailItem)?
|
||||||
|
@ -152,16 +152,27 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, keepFirst: Bool = false) {
|
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] = []
|
var updateItems: [GalleryPagerUpdateItem] = []
|
||||||
let deleteItems: [Int] = []
|
var deleteItems: [Int] = []
|
||||||
var insertItems: [GalleryPagerInsertItem] = []
|
var insertItems: [GalleryPagerInsertItem] = []
|
||||||
for i in 0 ..< items.count {
|
var previousIndexById: [AnyHashable: Int] = [:]
|
||||||
if i == 0 && keepFirst {
|
var validIds = Set(items.map { $0.id })
|
||||||
updateItems.append(GalleryPagerUpdateItem(index: 0, previousIndex: 0, item: items[i]))
|
|
||||||
} else {
|
for i in 0 ..< self.items.count {
|
||||||
insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: nil))
|
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))
|
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 {
|
for updatedItem in transaction.updateItems {
|
||||||
self.items[updatedItem.previousIndex] = updatedItem.item
|
self.items[updatedItem.previousIndex] = updatedItem.item
|
||||||
if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) {
|
if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) {
|
||||||
|
//print("update visible node at \(updatedItem.previousIndex)")
|
||||||
updatedItem.item.updateNode(node: itemNode)
|
updatedItem.item.updateNode(node: itemNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,55 +192,52 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.items.remove(at: deleteItemIndex)
|
self.items.remove(at: deleteItemIndex)
|
||||||
for i in 0 ..< self.itemNodes.count {
|
for i in 0 ..< self.itemNodes.count {
|
||||||
if self.itemNodes[i].index == deleteItemIndex {
|
if self.itemNodes[i].index == deleteItemIndex {
|
||||||
|
//print("delete visible node at \(deleteItemIndex)")
|
||||||
self.removeVisibleItemNode(internalIndex: i)
|
self.removeVisibleItemNode(internalIndex: i)
|
||||||
break
|
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 })
|
let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index })
|
||||||
if self.items.count == 0 && !insertItems.isEmpty {
|
|
||||||
if insertItems[0].index != 0 {
|
if transaction.updateItems.isEmpty && !insertItems.isEmpty {
|
||||||
fatalError("transaction: invalid insert into empty list")
|
self.items.removeAll()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for insertedItem in insertItems {
|
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 {
|
for itemNode in self.itemNodes {
|
||||||
var indexOffset = 0
|
if let remappedIndex = remapIndices[itemNode.index] {
|
||||||
for insertedItem in sortedInsertItems {
|
//print("remap visible node \(itemNode.index) -> \(remappedIndex)")
|
||||||
if insertedItem.index <= itemNode.index + indexOffset {
|
itemNode.index = remappedIndex
|
||||||
indexOffset += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
self.invalidatedItems = true
|
||||||
if let focusOnItem = transaction.focusOnItem {
|
if let focusOnItem = transaction.focusOnItem {
|
||||||
self.centralItemIndex = focusOnItem
|
self.centralItemIndex = focusOnItem
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateItemNodes(transition: .immediate)
|
self.updateItemNodes(transition: .immediate)
|
||||||
|
|
||||||
|
//print("visible indices after update \(self.itemNodes.map { $0.index })")
|
||||||
}
|
}
|
||||||
else if let focusOnItem = transaction.focusOnItem {
|
else if let focusOnItem = transaction.focusOnItem {
|
||||||
self.ignoreCentralItemIndexUpdate = true
|
self.ignoreCentralItemIndexUpdate = true
|
||||||
|
@ -15,6 +15,10 @@ import StickerResources
|
|||||||
import AppBundle
|
import AppBundle
|
||||||
|
|
||||||
class ChatAnimationGalleryItem: GalleryItem {
|
class ChatAnimationGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.message.stableId
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let message: Message
|
let message: Message
|
||||||
|
@ -12,6 +12,10 @@ import AccountContext
|
|||||||
import RadialStatusNode
|
import RadialStatusNode
|
||||||
|
|
||||||
class ChatDocumentGalleryItem: GalleryItem {
|
class ChatDocumentGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.message.stableId
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let message: Message
|
let message: Message
|
||||||
|
@ -13,6 +13,10 @@ import RadialStatusNode
|
|||||||
import ShareController
|
import ShareController
|
||||||
|
|
||||||
class ChatExternalFileGalleryItem: GalleryItem {
|
class ChatExternalFileGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.message.stableId
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let message: Message
|
let message: Message
|
||||||
|
@ -79,6 +79,10 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChatImageGalleryItem: GalleryItem {
|
class ChatImageGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.message.stableId
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let message: Message
|
let message: Message
|
||||||
|
@ -19,6 +19,10 @@ public enum UniversalVideoGalleryItemContentInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class UniversalVideoGalleryItem: GalleryItem {
|
public class UniversalVideoGalleryItem: GalleryItem {
|
||||||
|
public var id: AnyHashable {
|
||||||
|
return self.content.id
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let content: UniversalVideoContent
|
let content: UniversalVideoContent
|
||||||
|
@ -35,6 +35,12 @@ private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InstantImageGalleryItem: GalleryItem {
|
class InstantImageGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemId: AnyHashable
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let imageReference: ImageMediaReference
|
let imageReference: ImageMediaReference
|
||||||
@ -44,7 +50,8 @@ class InstantImageGalleryItem: GalleryItem {
|
|||||||
let openUrl: (InstantPageUrlItem) -> Void
|
let openUrl: (InstantPageUrlItem) -> Void
|
||||||
let openUrlOptions: (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.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.imageReference = imageReference
|
self.imageReference = imageReference
|
||||||
|
@ -98,7 +98,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let image = self.media.media as? TelegramMediaImage {
|
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 {
|
} else if let file = self.media.media as? TelegramMediaFile {
|
||||||
if file.isVideo {
|
if file.isVideo {
|
||||||
var indexData: GalleryItemIndexData?
|
var indexData: GalleryItemIndexData?
|
||||||
@ -121,7 +121,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource))
|
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: [])
|
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 {
|
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content {
|
||||||
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) {
|
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) {
|
||||||
|
@ -31,7 +31,7 @@ struct SecureIdDocumentGalleryEntry: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func item(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, delete: @escaping (TelegramMediaResource) -> Void) -> GalleryItem {
|
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)
|
delete(self.resource)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,12 @@ import PhotoResources
|
|||||||
import GalleryUI
|
import GalleryUI
|
||||||
|
|
||||||
class SecureIdDocumentGalleryItem: GalleryItem {
|
class SecureIdDocumentGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemId: AnyHashable
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
@ -21,7 +27,8 @@ class SecureIdDocumentGalleryItem: GalleryItem {
|
|||||||
let location: SecureIdDocumentGalleryEntryLocation
|
let location: SecureIdDocumentGalleryEntryLocation
|
||||||
let delete: () -> Void
|
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.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
@ -11,15 +11,29 @@ import TelegramPresentationData
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import GalleryUI
|
import GalleryUI
|
||||||
|
|
||||||
|
public enum AvatarGalleryEntryId: Hashable {
|
||||||
|
case topImage
|
||||||
|
case image(MediaId)
|
||||||
|
}
|
||||||
|
|
||||||
public enum AvatarGalleryEntry: Equatable {
|
public enum AvatarGalleryEntry: Equatable {
|
||||||
case topImage([ImageRepresentationWithReference], GalleryItemIndexData?)
|
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] {
|
public var representations: [ImageRepresentationWithReference] {
|
||||||
switch self {
|
switch self {
|
||||||
case let .topImage(representations, _):
|
case let .topImage(representations, _):
|
||||||
return representations
|
return representations
|
||||||
case let .image(_, representations, _, _, _, _):
|
case let .image(_, _, representations, _, _, _, _):
|
||||||
return representations
|
return representations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,7 +42,7 @@ public enum AvatarGalleryEntry: Equatable {
|
|||||||
switch self {
|
switch self {
|
||||||
case let .topImage(_, indexData):
|
case let .topImage(_, indexData):
|
||||||
return indexData
|
return indexData
|
||||||
case let .image(_, _, _, _, indexData, _):
|
case let .image(_, _, _, _, _, indexData, _):
|
||||||
return indexData
|
return indexData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,8 +55,8 @@ public enum AvatarGalleryEntry: Equatable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .image(lhsImageReference, lhsRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId):
|
case let .image(lhsId, 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 {
|
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
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -84,9 +98,9 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<
|
|||||||
for photo in photos {
|
for photo in photos {
|
||||||
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count))
|
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count))
|
||||||
if result.isEmpty, let first = initialEntries.first {
|
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 {
|
} 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
|
index += 1
|
||||||
}
|
}
|
||||||
@ -111,9 +125,9 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry
|
|||||||
for photo in photos {
|
for photo in photos {
|
||||||
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count))
|
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count))
|
||||||
if result.isEmpty, let first = initialEntries.first {
|
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 {
|
} 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
|
index += 1
|
||||||
}
|
}
|
||||||
@ -130,6 +144,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peer: Peer
|
private let peer: Peer
|
||||||
|
private let sourceHasRoundCorners: Bool
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
|
|
||||||
@ -159,12 +174,15 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
|
|
||||||
private let replaceRootController: (ViewController, ValuePromise<Bool>?) -> Void
|
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.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.sourceHasRoundCorners = sourceHasRoundCorners
|
||||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
self.replaceRootController = replaceRootController
|
self.replaceRootController = replaceRootController
|
||||||
|
|
||||||
|
self.centralEntryIndex = centralEntryIndex
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
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))
|
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 = {
|
let f: () -> Void = {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.entries = entries
|
strongSelf.entries = entries
|
||||||
strongSelf.centralEntryIndex = 0
|
if strongSelf.centralEntryIndex == nil {
|
||||||
|
strongSelf.centralEntryIndex = 0
|
||||||
|
}
|
||||||
if strongSelf.isViewLoaded {
|
if strongSelf.isViewLoaded {
|
||||||
let canDelete: Bool
|
let canDelete: Bool
|
||||||
if strongSelf.peer.id == strongSelf.context.account.peerId {
|
if strongSelf.peer.id == strongSelf.context.account.peerId {
|
||||||
@ -213,7 +233,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
} else {
|
} else {
|
||||||
canDelete = false
|
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)
|
self?.deleteEntry(entry)
|
||||||
} : nil) }), centralItemIndex: 0, keepFirst: true)
|
} : 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 let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments {
|
||||||
if !self.entries.isEmpty {
|
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
|
animatedOutNode = false
|
||||||
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
|
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
|
||||||
animatedOutNode = true
|
animatedOutNode = true
|
||||||
@ -333,7 +353,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
self.galleryNode.transitionDataForCentralItem = { [weak self] in
|
self.galleryNode.transitionDataForCentralItem = { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? AvatarGalleryControllerPresentationArguments {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
|
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
|
||||||
@ -365,7 +385,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = self.presentationData
|
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)
|
self?.deleteEntry(entry)
|
||||||
} : nil) }), centralItemIndex: self.centralEntryIndex)
|
} : 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 self.peer.id == self.context.account.peerId {
|
||||||
if let reference = reference {
|
if let reference = reference {
|
||||||
let _ = removeAccountPhoto(network: self.context.account.network, reference: reference).start()
|
let _ = removeAccountPhoto(network: self.context.account.network, reference: reference).start()
|
||||||
|
@ -84,7 +84,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
|
|||||||
var nameText: String?
|
var nameText: String?
|
||||||
var dateText: String?
|
var dateText: String?
|
||||||
switch entry {
|
switch entry {
|
||||||
case let .image(_, _, peer, date, _, _):
|
case let .image(_, _, _, peer, date, _, _):
|
||||||
nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
||||||
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date)
|
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date)
|
||||||
default:
|
default:
|
||||||
|
@ -42,22 +42,28 @@ private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PeerAvatarImageGalleryItem: GalleryItem {
|
class PeerAvatarImageGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.entry.id
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let peer: Peer
|
let peer: Peer
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let entry: AvatarGalleryEntry
|
let entry: AvatarGalleryEntry
|
||||||
|
let sourceHasRoundCorners: Bool
|
||||||
let delete: (() -> Void)?
|
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.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
|
self.sourceHasRoundCorners = sourceHasRoundCorners
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
}
|
}
|
||||||
|
|
||||||
func node() -> GalleryItemNode {
|
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 {
|
if let indexData = self.entry.indexData {
|
||||||
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0))
|
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 {
|
switch self.entry {
|
||||||
case let .topImage(representations, _):
|
case let .topImage(representations, _):
|
||||||
content = representations
|
content = representations
|
||||||
case let .image(_, representations, _, _, _, _):
|
case let .image(_, _, representations, _, _, _, _):
|
||||||
content = representations
|
content = representations
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +102,7 @@ class PeerAvatarImageGalleryItem: GalleryItem {
|
|||||||
final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peer: Peer
|
private let peer: Peer
|
||||||
|
private let sourceHasRoundCorners: Bool
|
||||||
|
|
||||||
private var entry: AvatarGalleryEntry?
|
private var entry: AvatarGalleryEntry?
|
||||||
|
|
||||||
@ -110,9 +117,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
private var status: MediaResourceStatus?
|
private var status: MediaResourceStatus?
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, peer: Peer) {
|
init(context: AccountContext, presentationData: PresentationData, peer: Peer, sourceHasRoundCorners: Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.sourceHasRoundCorners = sourceHasRoundCorners
|
||||||
|
|
||||||
self.imageNode = TransformImageNode()
|
self.imageNode = TransformImageNode()
|
||||||
self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData)
|
self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData)
|
||||||
@ -175,7 +183,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
switch entry {
|
switch entry {
|
||||||
case let .topImage(topRepresentations, _):
|
case let .topImage(topRepresentations, _):
|
||||||
representations = topRepresentations
|
representations = topRepresentations
|
||||||
case let .image(_, imageRepresentations, _, _, _, _):
|
case let .image(_, _, imageRepresentations, _, _, _, _):
|
||||||
representations = imageRepresentations
|
representations = imageRepresentations
|
||||||
}
|
}
|
||||||
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations), dispatchOnDisplayLink: false)
|
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 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 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 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.frame = transformedSelfFrame
|
||||||
|
|
||||||
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in
|
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.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.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 self.sourceHasRoundCorners {
|
||||||
if value {
|
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
|
||||||
self?.imageNode.clipsToBounds = false
|
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.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)
|
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 positionCompleted = false
|
||||||
var boundsCompleted = false
|
var boundsCompleted = false
|
||||||
var copyCompleted = false
|
var copyCompleted = false
|
||||||
|
var surfaceCopyCompleted = false
|
||||||
|
|
||||||
let copyView = node.2().0!
|
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
|
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 {
|
if positionCompleted && boundsCompleted && copyCompleted {
|
||||||
copyView?.removeFromSuperview()
|
copyView?.removeFromSuperview()
|
||||||
|
surfaceCopyView?.removeFromSuperview()
|
||||||
completion()
|
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)
|
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.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.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)
|
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 {
|
switch entry {
|
||||||
case let .topImage(topRepresentations, _):
|
case let .topImage(topRepresentations, _):
|
||||||
representations = topRepresentations
|
representations = topRepresentations
|
||||||
case let .image(_, imageRepresentations, _, _, _, _):
|
case let .image(_, _, imageRepresentations, _, _, _, _):
|
||||||
representations = imageRepresentations
|
representations = imageRepresentations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,7 +297,7 @@ public class WallpaperGalleryController: ViewController {
|
|||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
var updateItems: [GalleryPagerUpdateItem] = []
|
var updateItems: [GalleryPagerUpdateItem] = []
|
||||||
for entry in entries {
|
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)
|
updateItems.append(item)
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
@ -660,7 +660,7 @@ public class WallpaperGalleryController: ViewController {
|
|||||||
colors = true
|
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 {
|
if let initialOptions = self.initialOptions, let itemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode {
|
||||||
itemNode.options = initialOptions
|
itemNode.options = initialOptions
|
||||||
|
@ -32,13 +32,20 @@ struct WallpaperGalleryItemArguments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WallpaperGalleryItem: GalleryItem {
|
class WallpaperGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.index
|
||||||
|
}
|
||||||
|
|
||||||
|
let index: Int
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let entry: WallpaperGalleryEntry
|
let entry: WallpaperGalleryEntry
|
||||||
let arguments: WallpaperGalleryItemArguments
|
let arguments: WallpaperGalleryItemArguments
|
||||||
let source: WallpaperListSource
|
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.context = context
|
||||||
|
self.index = index
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.arguments = arguments
|
self.arguments = arguments
|
||||||
self.source = source
|
self.source = source
|
||||||
|
@ -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
|
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in
|
||||||
f(.dismissWithoutContent)
|
f(.dismissWithoutContent)
|
||||||
self?.navigationButtonAction(.openChatInfo(expandAvatar: true))
|
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)
|
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||||
|
@ -77,11 +77,11 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if case .standard(true) = presentationInterfaceState.mode {
|
if case .standard(true) = presentationInterfaceState.mode {
|
||||||
return nil
|
return chatInfoNavigationButton
|
||||||
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
|
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
|
||||||
if presentationInterfaceState.accountPeerId == peer.id {
|
if presentationInterfaceState.accountPeerId == peer.id {
|
||||||
if presentationInterfaceState.isScheduledMessages {
|
if presentationInterfaceState.isScheduledMessages {
|
||||||
return nil
|
return chatInfoNavigationButton
|
||||||
} else {
|
} else {
|
||||||
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
||||||
buttonItem.accessibilityLabel = strings.Conversation_Search
|
buttonItem.accessibilityLabel = strings.Conversation_Search
|
||||||
|
@ -46,7 +46,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
|
|||||||
switch action.action {
|
switch action.action {
|
||||||
case let .photoUpdated(image):
|
case let .photoUpdated(image):
|
||||||
if let peer = messageMainPeer(message), let image = 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
|
let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: promise, replaceRootController: { controller, ready in
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -23,6 +23,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem {
|
|||||||
let action: (() -> Void)?
|
let action: (() -> Void)?
|
||||||
let longTapAction: ((ASDisplayNode) -> Void)?
|
let longTapAction: ((ASDisplayNode) -> Void)?
|
||||||
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
|
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
|
||||||
|
let requestLayout: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: AnyHashable,
|
id: AnyHashable,
|
||||||
@ -32,7 +33,8 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem {
|
|||||||
textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine,
|
textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine,
|
||||||
action: (() -> Void)?,
|
action: (() -> Void)?,
|
||||||
longTapAction: ((ASDisplayNode) -> Void)? = nil,
|
longTapAction: ((ASDisplayNode) -> Void)? = nil,
|
||||||
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil
|
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil,
|
||||||
|
requestLayout: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.label = label
|
self.label = label
|
||||||
@ -42,6 +44,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem {
|
|||||||
self.action = action
|
self.action = action
|
||||||
self.longTapAction = longTapAction
|
self.longTapAction = longTapAction
|
||||||
self.linkItemAction = linkItemAction
|
self.linkItemAction = linkItemAction
|
||||||
|
self.requestLayout = requestLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
func node() -> PeerInfoScreenItemNode {
|
func node() -> PeerInfoScreenItemNode {
|
||||||
@ -55,11 +58,16 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
private let textNode: ImmediateTextNode
|
private let textNode: ImmediateTextNode
|
||||||
private let bottomSeparatorNode: ASDisplayNode
|
private let bottomSeparatorNode: ASDisplayNode
|
||||||
|
|
||||||
|
private let expandNode: ImmediateTextNode
|
||||||
|
private let expandButonNode: HighlightTrackingButtonNode
|
||||||
|
|
||||||
private var linkHighlightingNode: LinkHighlightingNode?
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
|
|
||||||
private var item: PeerInfoScreenLabeledValueItem?
|
private var item: PeerInfoScreenLabeledValueItem?
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
|
|
||||||
|
private var isExpanded: Bool = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
var bringToFrontForHighlightImpl: (() -> Void)?
|
var bringToFrontForHighlightImpl: (() -> Void)?
|
||||||
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
|
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
|
||||||
@ -76,6 +84,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
self.bottomSeparatorNode = ASDisplayNode()
|
self.bottomSeparatorNode = ASDisplayNode()
|
||||||
self.bottomSeparatorNode.isLayerBacked = true
|
self.bottomSeparatorNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.expandNode = ImmediateTextNode()
|
||||||
|
self.expandNode.displaysAsynchronously = false
|
||||||
|
self.expandNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.expandButonNode = HighlightTrackingButtonNode()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
bringToFrontForHighlightImpl = { [weak self] in
|
bringToFrontForHighlightImpl = { [weak self] in
|
||||||
@ -86,6 +100,27 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
self.addSubnode(self.selectionNode)
|
self.addSubnode(self.selectionNode)
|
||||||
self.addSubnode(self.labelNode)
|
self.addSubnode(self.labelNode)
|
||||||
self.addSubnode(self.textNode)
|
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() {
|
override func didLoad() {
|
||||||
@ -96,6 +131,9 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return .keepWithSingleTap
|
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) {
|
if let _ = strongSelf.linkItemAtPoint(point) {
|
||||||
return .waitForSingleTap
|
return .waitForSingleTap
|
||||||
}
|
}
|
||||||
@ -162,14 +200,19 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
textColorValue = presentationData.theme.list.itemAccentColor
|
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)
|
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||||
|
|
||||||
switch item.textBehavior {
|
switch item.textBehavior {
|
||||||
case .singleLine:
|
case .singleLine:
|
||||||
|
self.textNode.cutout = nil
|
||||||
self.textNode.maximumNumberOfLines = 1
|
self.textNode.maximumNumberOfLines = 1
|
||||||
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
||||||
case let .multiLine(maxLines, enabledEntities):
|
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 {
|
if enabledEntities.isEmpty {
|
||||||
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
||||||
} else {
|
} else {
|
||||||
@ -188,11 +231,24 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
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 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 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.labelNode, frame: labelFrame)
|
||||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||||
|
|
||||||
|
@ -133,8 +133,18 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
|
|
||||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
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
|
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||||
|
|
||||||
|
@ -77,6 +77,14 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
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))
|
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
|
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +177,16 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
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
|
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||||
|
|
||||||
|
@ -106,6 +106,10 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelPreviewGesture() {
|
||||||
|
self.containerNode.cancelGesture()
|
||||||
|
}
|
||||||
|
|
||||||
func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) {
|
func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) {
|
||||||
if item === self.item?.0 && size == self.item?.2 {
|
if item === self.item?.0 && size == self.item?.2 {
|
||||||
return
|
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)
|
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous)
|
||||||
|
|
||||||
if isScrollingLockedAtTop {
|
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
|
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
|
||||||
}
|
}
|
||||||
@ -561,6 +567,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
self.decelerationAnimator?.isPaused = true
|
self.decelerationAnimator?.isPaused = true
|
||||||
self.decelerationAnimator = nil
|
self.decelerationAnimator = nil
|
||||||
|
|
||||||
|
for (_, itemNode) in self.visibleMediaItems {
|
||||||
|
itemNode.cancelPreviewGesture()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
@ -168,7 +168,7 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.imageNode.contentAnimations = .subsequentUpdates
|
self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||||
self.addSubnode(self.imageNode)
|
self.addSubnode(self.imageNode)
|
||||||
|
|
||||||
self.imageNode.imageUpdated = { [weak self] _ in
|
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) {
|
init(context: AccountContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -406,7 +414,15 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
func selectFirstItem() {
|
func selectFirstItem() {
|
||||||
self.currentIndex = 0
|
self.currentIndex = 0
|
||||||
if let size = self.validLayout {
|
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 location.x < size.width * 1.0 / 5.0 {
|
||||||
if self.currentIndex != 0 {
|
if self.currentIndex != 0 {
|
||||||
self.currentIndex -= 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 {
|
} else if self.items.count > 1 {
|
||||||
self.currentIndex = 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 {
|
} else {
|
||||||
if self.currentIndex < self.items.count - 1 {
|
if self.currentIndex < self.items.count - 1 {
|
||||||
self.currentIndex += 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 {
|
} else if self.items.count > 1 {
|
||||||
self.currentIndex = 0
|
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
|
self.transitionFraction = transitionFraction
|
||||||
if let size = self.validLayout {
|
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:
|
case .cancelled, .ended:
|
||||||
let translation = recognizer.translation(in: self.view)
|
let translation = recognizer.translation(in: self.view)
|
||||||
@ -472,7 +488,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
self.currentIndex = updatedIndex
|
self.currentIndex = updatedIndex
|
||||||
self.transitionFraction = 0.0
|
self.transitionFraction = 0.0
|
||||||
if let size = self.validLayout {
|
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:
|
default:
|
||||||
break
|
break
|
||||||
@ -497,14 +513,14 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
switch entry {
|
switch entry {
|
||||||
case let .topImage(representations, _):
|
case let .topImage(representations, _):
|
||||||
items.append(.topImage(representations))
|
items.append(.topImage(representations))
|
||||||
case let .image(reference, representations, _, _, _, _):
|
case let .image(_, reference, representations, _, _, _, _):
|
||||||
items.append(.image(reference, representations))
|
items.append(.image(reference, representations))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.galleryEntries = entries
|
strongSelf.galleryEntries = entries
|
||||||
strongSelf.items = items
|
strongSelf.items = items
|
||||||
if let size = strongSelf.validLayout {
|
if let size = strongSelf.validLayout {
|
||||||
strongSelf.updateItems(size: size, transition: .immediate)
|
strongSelf.updateItems(size: size, transition: .immediate, stripTransition: .immediate)
|
||||||
}
|
}
|
||||||
if items.isEmpty {
|
if items.isEmpty {
|
||||||
if !strongSelf.didSetReady {
|
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 validIds: [WrappedMediaResourceId] = []
|
||||||
var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = []
|
var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = []
|
||||||
var additiveTransitionOffset: CGFloat = 0.0
|
var additiveTransitionOffset: CGFloat = 0.0
|
||||||
@ -603,15 +619,20 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
let stripInset: CGFloat = 8.0
|
let stripInset: CGFloat = 8.0
|
||||||
let stripSpacing: CGFloat = 4.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)))
|
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 {
|
for i in 0 ..< self.stripNodes.count {
|
||||||
|
let stripX: CGFloat = stripInset + CGFloat(i) * (stripWidth + stripSpacing)
|
||||||
if i == 0 && self.stripNodes.count == 1 {
|
if i == 0 && self.stripNodes.count == 1 {
|
||||||
self.stripNodes[i].isHidden = true
|
self.stripNodes[i].isHidden = true
|
||||||
} else {
|
} else {
|
||||||
self.stripNodes[i].isHidden = false
|
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))
|
let stripFrame = CGRect(origin: CGPoint(x: stripOffset + stripX, y: 0.0), size: CGSize(width: stripWidth + 1.0, height: 2.0))
|
||||||
stripX += stripWidth + stripSpacing
|
stripTransition.updateFrame(node: self.stripNodes[i], frame: stripFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let item = self.items.first, let itemNode = self.itemNodes[item.id] {
|
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
|
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 textNode: TextFieldNode
|
||||||
|
private let clearIconNode: ASImageNode
|
||||||
|
private let clearButtonNode: HighlightableButtonNode
|
||||||
private let topSeparator: ASDisplayNode
|
private let topSeparator: ASDisplayNode
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
@ -1059,20 +1082,69 @@ final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeader
|
|||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.textNode = TextFieldNode()
|
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()
|
self.topSeparator = ASDisplayNode()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.textNode)
|
self.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.clearIconNode)
|
||||||
|
self.addSubnode(self.clearButtonNode)
|
||||||
self.addSubnode(self.topSeparator)
|
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 {
|
func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat {
|
||||||
if self.theme !== presentationData.theme {
|
if self.theme !== presentationData.theme {
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor
|
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.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)
|
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
|
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.isUserInteractionEnabled = isEnabled
|
||||||
self.textNode.alpha = isEnabled ? 1.0 : 0.6
|
self.textNode.alpha = isEnabled ? 1.0 : 0.6
|
||||||
@ -1103,6 +1181,8 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT
|
|||||||
private let textNode: EditableTextNode
|
private let textNode: EditableTextNode
|
||||||
private let textNodeContainer: ASDisplayNode
|
private let textNodeContainer: ASDisplayNode
|
||||||
private let measureTextNode: ImmediateTextNode
|
private let measureTextNode: ImmediateTextNode
|
||||||
|
private let clearIconNode: ASImageNode
|
||||||
|
private let clearButtonNode: HighlightableButtonNode
|
||||||
private let topSeparator: ASDisplayNode
|
private let topSeparator: ASDisplayNode
|
||||||
|
|
||||||
private let requestUpdateHeight: () -> Void
|
private let requestUpdateHeight: () -> Void
|
||||||
@ -1124,11 +1204,45 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT
|
|||||||
self.measureTextNode.maximumNumberOfLines = 0
|
self.measureTextNode.maximumNumberOfLines = 0
|
||||||
self.topSeparator = ASDisplayNode()
|
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()
|
super.init()
|
||||||
|
|
||||||
self.textNodeContainer.addSubnode(self.textNode)
|
self.textNodeContainer.addSubnode(self.textNode)
|
||||||
self.addSubnode(self.textNodeContainer)
|
self.addSubnode(self.textNodeContainer)
|
||||||
|
self.addSubnode(self.clearIconNode)
|
||||||
|
self.addSubnode(self.clearButtonNode)
|
||||||
self.addSubnode(self.topSeparator)
|
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 {
|
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.clipsToBounds = true
|
||||||
self.textNode.delegate = self
|
self.textNode.delegate = self
|
||||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
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
|
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)
|
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
|
||||||
self.measureTextNode.attributedText = attributedMeasureText
|
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
|
self.currentMeasuredHeight = measureTextSize.height
|
||||||
|
|
||||||
let height = measureTextSize.height + 22.0
|
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.textNodeContainer.frame = textNodeFrame
|
||||||
self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size)
|
self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size)
|
||||||
|
|
||||||
return height
|
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 {
|
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
guard let theme = self.theme else {
|
guard let theme = self.theme else {
|
||||||
return true
|
return true
|
||||||
@ -1239,6 +1376,10 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
|||||||
return self.itemNodes[key]?.text
|
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 {
|
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 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))
|
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
|
let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode
|
||||||
|
|
||||||
var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)?
|
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 requestOpenAvatarForEditing: (() -> Void)?
|
||||||
var requestUpdateLayout: (() -> Void)?
|
var requestUpdateLayout: (() -> Void)?
|
||||||
|
|
||||||
@ -1441,13 +1582,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.navigationButtonContainer)
|
self.addSubnode(self.navigationButtonContainer)
|
||||||
|
|
||||||
self.avatarListNode.avatarContainerNode.tapped = { [weak self] in
|
self.avatarListNode.avatarContainerNode.tapped = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
self?.initiateAvatarExpansion()
|
||||||
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.editingContentNode.avatarNode.tapped = { [weak self] in
|
self.editingContentNode.avatarNode.tapped = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1457,8 +1592,51 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAvatarIsHidden(_ isHidden: Bool) {
|
func initiateAvatarExpansion() {
|
||||||
self.avatarListNode.avatarContainerNode.avatarNode.isHidden = isHidden
|
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 {
|
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 {
|
||||||
|
@ -26,6 +26,7 @@ protocol PeerInfoPaneNode: ASDisplayNode {
|
|||||||
final class PeerInfoPaneWrapper {
|
final class PeerInfoPaneWrapper {
|
||||||
let key: PeerInfoPaneKey
|
let key: PeerInfoPaneKey
|
||||||
let node: PeerInfoPaneNode
|
let node: PeerInfoPaneNode
|
||||||
|
var isAnimatingOut: Bool = false
|
||||||
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)?
|
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)?
|
||||||
|
|
||||||
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
||||||
@ -114,6 +115,10 @@ struct PeerInfoPaneSpecifier: Equatable {
|
|||||||
var title: String
|
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 {
|
final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:]
|
private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:]
|
||||||
@ -148,7 +153,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
|||||||
self.scrollNode.addSubnode(self.selectedLineNode)
|
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))
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
let focusOnSelectedPane = self.currentParams?.1 != selectedPane
|
let focusOnSelectedPane = self.currentParams?.1 != selectedPane
|
||||||
@ -192,8 +197,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
|||||||
|
|
||||||
var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode, Bool)] = []
|
var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode, Bool)] = []
|
||||||
var totalRawTabSize: CGFloat = 0.0
|
var totalRawTabSize: CGFloat = 0.0
|
||||||
|
var selectionFrames: [CGRect] = []
|
||||||
|
|
||||||
var selectedFrame: CGRect?
|
|
||||||
for specifier in paneList {
|
for specifier in paneList {
|
||||||
guard let paneNode = self.paneNodes[specifier.key] else {
|
guard let paneNode = self.paneNodes[specifier.key] else {
|
||||||
continue
|
continue
|
||||||
@ -208,8 +213,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
|||||||
totalRawTabSize += paneNodeSize.width
|
totalRawTabSize += paneNodeSize.width
|
||||||
}
|
}
|
||||||
|
|
||||||
let spacing: CGFloat = 32.0
|
let minSpacing: CGFloat = 10.0
|
||||||
if tabSizes.count == 1 {
|
if tabSizes.count <= 1 {
|
||||||
for i in 0 ..< tabSizes.count {
|
for i in 0 ..< tabSizes.count {
|
||||||
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
||||||
let leftOffset: CGFloat = 16.0
|
let leftOffset: CGFloat = 16.0
|
||||||
@ -226,36 +231,63 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
|||||||
paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset)
|
paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset)
|
||||||
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset)
|
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset)
|
||||||
|
|
||||||
if paneList[i].key == selectedPane {
|
selectionFrames.append(paneFrame)
|
||||||
selectedFrame = paneFrame
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height)
|
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 availableSpace = size.width
|
||||||
let availableSpacing = availableSpace - totalRawTabSize
|
let availableSpacing = availableSpace - totalRawTabSize
|
||||||
let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1))
|
let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1))
|
||||||
|
|
||||||
var leftOffset = perTabSpacing
|
let normalizedPerTabWidth = floor(availableSpace / CGFloat(tabSizes.count))
|
||||||
for i in 0 ..< tabSizes.count {
|
var maxSpacing: CGFloat = 0.0
|
||||||
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
var minSpacing: CGFloat = .greatestFiniteMagnitude
|
||||||
|
for i in 0 ..< tabSizes.count - 1 {
|
||||||
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
|
let distanceToNextBoundary = (normalizedPerTabWidth - tabSizes[i].0.width) / 2.0
|
||||||
if wasAdded {
|
let nextDistanceToBoundary = (normalizedPerTabWidth - tabSizes[i + 1].0.width) / 2.0
|
||||||
paneNode.frame = paneFrame
|
let distance = nextDistanceToBoundary + distanceToNextBoundary
|
||||||
paneNode.alpha = 0.0
|
maxSpacing = max(distance, maxSpacing)
|
||||||
transition.updateAlpha(node: paneNode, alpha: 1.0)
|
minSpacing = min(distance, minSpacing)
|
||||||
} else {
|
}
|
||||||
transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame)
|
|
||||||
|
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)
|
} else {
|
||||||
paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset)
|
var leftOffset = perTabSpacing
|
||||||
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset)
|
for i in 0 ..< tabSizes.count {
|
||||||
|
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
||||||
leftOffset += paneNodeSize.width + perTabSpacing
|
|
||||||
|
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
|
||||||
if paneList[i].key == selectedPane {
|
if wasAdded {
|
||||||
selectedFrame = paneFrame
|
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)
|
self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height)
|
||||||
@ -272,14 +304,29 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame)
|
transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame)
|
||||||
}
|
}
|
||||||
paneNode.updateArea(size: paneFrame.size, sideInset: spacing)
|
paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing)
|
||||||
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing, bottom: 0.0, right: -spacing)
|
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing, bottom: 0.0, right: -minSpacing)
|
||||||
if paneList[i].key == selectedPane {
|
|
||||||
selectedFrame = paneFrame
|
selectionFrames.append(paneFrame)
|
||||||
}
|
|
||||||
leftOffset += paneNodeSize.width + spacing
|
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 {
|
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 context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: PeerId
|
||||||
|
|
||||||
@ -326,11 +426,22 @@ final class PeerInfoPaneContainerNode: ASDisplayNode {
|
|||||||
var didSetIsReady = false
|
var didSetIsReady = false
|
||||||
|
|
||||||
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)?
|
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(set) var currentPaneKey: PeerInfoPaneKey?
|
||||||
private var candidatePane: (PeerInfoPaneWrapper, Disposable, Bool)?
|
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?
|
var selectionPanelNode: PeerInfoSelectionPanelNode?
|
||||||
|
|
||||||
@ -376,14 +487,95 @@ final class PeerInfoPaneContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strongSelf.currentCandidatePaneKey == key {
|
if strongSelf.currentPanes[key] != nil {
|
||||||
return
|
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) {
|
func updateSelectedMessageIds(_ selectedMessageIds: Set<MessageId>?, animated: Bool) {
|
||||||
self.currentPane?.node.updateSelectedMessages(animated: animated)
|
for (_, pane) in self.currentPanes {
|
||||||
self.candidatePane?.0.node.updateSelectedMessages(animated: animated)
|
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) {
|
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 ?? []
|
let availablePanes = data?.availablePanes ?? []
|
||||||
self.currentAvailablePanes = availablePanes
|
self.currentAvailablePanes = availablePanes
|
||||||
|
|
||||||
|
let previousCurrentPaneKey = self.currentPaneKey
|
||||||
|
|
||||||
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
|
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
|
||||||
var nextCandidatePaneKey: PeerInfoPaneKey?
|
var nextCandidatePaneKey: PeerInfoPaneKey?
|
||||||
if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 {
|
if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 {
|
||||||
@ -431,25 +629,21 @@ final class PeerInfoPaneContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let nextCandidatePaneKey = nextCandidatePaneKey {
|
if let nextCandidatePaneKey = nextCandidatePaneKey {
|
||||||
if self.currentCandidatePaneKey != nextCandidatePaneKey {
|
self.pendingSwitchToPaneKey = nextCandidatePaneKey
|
||||||
self.currentCandidatePaneKey = nextCandidatePaneKey
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.currentCandidatePaneKey = nil
|
self.currentPaneKey = nil
|
||||||
if let (_, disposable, _) = self.candidatePane {
|
self.pendingSwitchToPaneKey = nil
|
||||||
disposable.dispose()
|
|
||||||
self.candidatePane = nil
|
|
||||||
}
|
|
||||||
if let currentPane = self.currentPane {
|
|
||||||
self.currentPane = nil
|
|
||||||
currentPane.node.removeFromSupernode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if self.currentPaneKey == 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)
|
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))
|
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight))
|
||||||
|
|
||||||
if let currentCandidatePaneKey = self.currentCandidatePaneKey {
|
var visiblePaneIndices: [Int] = []
|
||||||
if self.candidatePane?.0.key != currentCandidatePaneKey {
|
var requiredPendingKeys: [PeerInfoPaneKey] = []
|
||||||
self.candidatePane?.1.dispose()
|
if let currentIndex = currentIndex {
|
||||||
|
if currentIndex != 0 {
|
||||||
let paneNode: PeerInfoPaneNode
|
visiblePaneIndices.append(currentIndex - 1)
|
||||||
switch currentCandidatePaneKey {
|
}
|
||||||
case .media:
|
visiblePaneIndices.append(currentIndex)
|
||||||
paneNode = PeerInfoVisualMediaPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId)
|
if currentIndex != availablePanes.count - 1 {
|
||||||
case .files:
|
visiblePaneIndices.append(currentIndex + 1)
|
||||||
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)
|
for index in visiblePaneIndices {
|
||||||
case .voice:
|
let indexOffset = CGFloat(index - currentIndex)
|
||||||
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .voiceOrInstantVideo)
|
let key = availablePanes[index]
|
||||||
case .music:
|
if self.currentPanes[key] == nil && self.pendingPanes[key] == nil {
|
||||||
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .music)
|
requiredPendingKeys.append(key)
|
||||||
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 {
|
if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey {
|
||||||
paneNode = PeerInfoMembersPaneNode(context: self.context, peerId: self.peerId, membersContext: membersContext, action: { [weak self] member, action in
|
if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil {
|
||||||
self?.requestPerformPeerMemberAction?(member, action)
|
if !requiredPendingKeys.contains(pendingSwitchToPaneKey) {
|
||||||
})
|
requiredPendingKeys.append(pendingSwitchToPaneKey)
|
||||||
} else {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
for key in requiredPendingKeys {
|
||||||
let previousPane = self.currentPane
|
if self.pendingPanes[key] == nil {
|
||||||
self.candidatePane = nil
|
var leftScope = false
|
||||||
self.currentPaneKey = candidatePane.key
|
let pane = PeerInfoPendingPane(
|
||||||
self.currentCandidatePaneKey = nil
|
context: self.context,
|
||||||
self.currentPane = candidatePane
|
chatControllerInteraction: self.chatControllerInteraction!,
|
||||||
|
data: data!,
|
||||||
if let selectionPanelNode = self.selectionPanelNode {
|
openPeerContextAction: { [weak self] peer, node, gesture in
|
||||||
self.insertSubnode(candidatePane.node, belowSubnode: selectionPanelNode)
|
self?.openPeerContextAction?(peer, node, gesture)
|
||||||
} else {
|
},
|
||||||
self.addSubnode(candidatePane.node)
|
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)))
|
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
|
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
|
||||||
let title: String
|
let title: String
|
||||||
@ -583,18 +836,18 @@ final class PeerInfoPaneContainerNode: ASDisplayNode {
|
|||||||
title = presentationData.strings.PeerInfo_PaneMembers
|
title = presentationData.strings.PeerInfo_PaneMembers
|
||||||
}
|
}
|
||||||
return PeerInfoPaneSpecifier(key: key, title: title)
|
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
|
let paneTransition: ContainedViewLayoutTransition = .immediate
|
||||||
paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame)
|
paneTransition.updateFrame(node: pane.pane.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)
|
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 !self.didSetIsReady && data != nil {
|
||||||
if let currentPane = self.currentPane {
|
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
||||||
self.didSetIsReady = true
|
self.didSetIsReady = true
|
||||||
self.isReady.set(currentPane.node.isReady)
|
self.isReady.set(currentPane.node.isReady)
|
||||||
} else if self.candidatePane == nil {
|
} else if self.pendingSwitchToPaneKey == nil {
|
||||||
self.didSetIsReady = true
|
self.didSetIsReady = true
|
||||||
self.isReady.set(.single(true))
|
self.isReady.set(.single(true))
|
||||||
}
|
}
|
||||||
|
@ -492,6 +492,7 @@ private final class PeerInfoInteraction {
|
|||||||
let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void
|
let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void
|
||||||
let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void
|
let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void
|
||||||
let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
|
let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
|
||||||
|
let requestLayout: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
openUsername: @escaping (String) -> Void,
|
openUsername: @escaping (String) -> Void,
|
||||||
@ -519,7 +520,8 @@ private final class PeerInfoInteraction {
|
|||||||
openPeerInfo: @escaping (Peer) -> Void,
|
openPeerInfo: @escaping (Peer) -> Void,
|
||||||
performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void,
|
performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void,
|
||||||
openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void,
|
openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void,
|
||||||
performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void
|
performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void,
|
||||||
|
requestLayout: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.openUsername = openUsername
|
self.openUsername = openUsername
|
||||||
self.openPhone = openPhone
|
self.openPhone = openPhone
|
||||||
@ -547,6 +549,7 @@ private final class PeerInfoInteraction {
|
|||||||
self.performMemberAction = performMemberAction
|
self.performMemberAction = performMemberAction
|
||||||
self.openPeerInfoContextMenu = openPeerInfoContextMenu
|
self.openPeerInfoContextMenu = openPeerInfoContextMenu
|
||||||
self.performBioLinkAction = performBioLinkAction
|
self.performBioLinkAction = performBioLinkAction
|
||||||
|
self.requestLayout = requestLayout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,6 +585,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openPhone(phone)
|
interaction.openPhone(phone)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode)
|
interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode)
|
||||||
|
}, requestLayout: {
|
||||||
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if let username = user.username {
|
if let username = user.username {
|
||||||
@ -589,13 +594,19 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openUsername(username)
|
interaction.openUsername(username)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
||||||
|
}, requestLayout: {
|
||||||
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if let cachedData = data.cachedData as? CachedUserData {
|
if let cachedData = data.cachedData as? CachedUserData {
|
||||||
if user.isScam {
|
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 {
|
} 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 {
|
if nearbyPeer {
|
||||||
@ -609,7 +620,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
} else {
|
} else {
|
||||||
if !data.isContact {
|
if !data.isContact {
|
||||||
if user.botInfo == nil {
|
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()
|
interaction.openAddContact()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -666,13 +677,19 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openUsername(username)
|
interaction.openUsername(username)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
||||||
|
}, requestLayout: {
|
||||||
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if let cachedData = data.cachedData as? CachedChannelData {
|
if let cachedData = data.cachedData as? CachedChannelData {
|
||||||
if channel.isScam {
|
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 {
|
} 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 {
|
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 {
|
} else if let group = data.peer as? TelegramGroup {
|
||||||
if let cachedData = data.cachedData as? CachedGroupData {
|
if let cachedData = data.cachedData as? CachedGroupData {
|
||||||
if group.isScam {
|
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 {
|
} 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: {
|
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: linkText, text: presentationData.strings.Channel_TypeSetup_Title, action: {
|
||||||
interaction.editingOpenPublicLinkSetup()
|
interaction.editingOpenPublicLinkSetup()
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) {
|
||||||
let discussionGroupTitle: String
|
let discussionGroupTitle: String
|
||||||
if let cachedData = data.cachedData as? CachedChannelData {
|
if let cachedData = data.cachedData as? CachedChannelData {
|
||||||
if let peer = data.linkedDiscussionPeer {
|
if let peer = data.linkedDiscussionPeer {
|
||||||
@ -1046,6 +1069,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
private let updateAvatarDisposable = MetaDisposable()
|
private let updateAvatarDisposable = MetaDisposable()
|
||||||
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
|
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
|
||||||
|
|
||||||
|
private var groupMembersSearchContext: GroupMembersSearchContext?
|
||||||
|
|
||||||
private let _ready = Promise<Bool>()
|
private let _ready = Promise<Bool>()
|
||||||
var ready: Promise<Bool> {
|
var ready: Promise<Bool> {
|
||||||
return self._ready
|
return self._ready
|
||||||
@ -1145,6 +1170,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
},
|
},
|
||||||
performBioLinkAction: { [weak self] action, item in
|
performBioLinkAction: { [weak self] action, item in
|
||||||
self?.performBioLinkAction(action: action, item: item)
|
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?.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 {
|
guard let strongSelf = self, let peer = strongSelf.data?.peer, peer.smallProfileImage != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let entriesPromise = Promise<[AvatarGalleryEntry]>(entries)
|
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
|
strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in
|
||||||
if entry == entries.first {
|
self?.headerNode.updateAvatarIsHidden(entry: entry)
|
||||||
self?.headerNode.updateAvatarIsHidden(true)
|
|
||||||
} else {
|
|
||||||
self?.headerNode.updateAvatarIsHidden(false)
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
strongSelf.view.endEditing(true)
|
strongSelf.view.endEditing(true)
|
||||||
strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { _ in
|
strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in
|
||||||
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ 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) ?? ""
|
let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? ""
|
||||||
|
|
||||||
if peer.firstName != firstName || peer.lastName != lastName {
|
if peer.firstName != firstName || peer.lastName != lastName {
|
||||||
strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName)
|
if firstName.isEmpty && lastName.isEmpty {
|
||||||
|> deliverOnMainQueue).start(error: { _ in
|
if strongSelf.hapticFeedback == nil {
|
||||||
guard let strongSelf = self else {
|
strongSelf.hapticFeedback = HapticFeedback()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
strongSelf.hapticFeedback?.error()
|
||||||
}, completed: {
|
strongSelf.headerNode.editingContentNode.shakeTextForKey(.firstName)
|
||||||
guard let strongSelf = self else {
|
} else {
|
||||||
return
|
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
|
strongSelf.controller?.present(statusController, in: .window(.root))
|
||||||
let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: peer.id)
|
strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName)
|
||||||
|> mapToSignal { peer, _ -> Signal<Void, NoError> in
|
|> deliverOnMainQueue).start(error: { _ in
|
||||||
guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else {
|
dismissStatus?()
|
||||||
return .complete()
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([]))
|
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||||
|> take(1)
|
}, completed: {
|
||||||
|> mapToSignal { records -> Signal<Void, NoError> in
|
dismissStatus?()
|
||||||
var signals: [Signal<DeviceContactExtendedData?, NoError>] = []
|
|
||||||
if let contactDataManager = context.sharedContext.contactDataManager {
|
guard let strongSelf = self else {
|
||||||
for (id, basicData) in records {
|
return
|
||||||
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))
|
}
|
||||||
}
|
let context = strongSelf.context
|
||||||
}
|
let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: peer.id)
|
||||||
return combineLatest(signals)
|
|> mapToSignal { peer, _ -> Signal<Void, NoError> in
|
||||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else {
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
}
|
return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([]))
|
||||||
}).start()
|
|> take(1)
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
|> 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 {
|
} else {
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||||
}
|
}
|
||||||
@ -1703,66 +1753,108 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
||||||
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
|
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
|
||||||
|
|
||||||
var updateDataSignals: [Signal<Never, Void>] = []
|
if title.isEmpty {
|
||||||
|
if strongSelf.hapticFeedback == nil {
|
||||||
if title != group.title {
|
strongSelf.hapticFeedback = HapticFeedback()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
strongSelf.hapticFeedback?.error()
|
||||||
}, completed: {
|
|
||||||
guard let strongSelf = self else {
|
strongSelf.headerNode.editingContentNode.shakeTextForKey(.title)
|
||||||
return
|
} 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) {
|
} else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(peer: channel) {
|
||||||
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
||||||
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
|
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
|
||||||
|
|
||||||
var updateDataSignals: [Signal<Never, Void>] = []
|
if title.isEmpty {
|
||||||
|
strongSelf.headerNode.editingContentNode.shakeTextForKey(.title)
|
||||||
if title != channel.title {
|
} else {
|
||||||
updateDataSignals.append(
|
var updateDataSignals: [Signal<Never, Void>] = []
|
||||||
updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title)
|
|
||||||
|> ignoreValues
|
if title != channel.title {
|
||||||
|> mapError { _ in return Void() }
|
updateDataSignals.append(
|
||||||
)
|
updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title)
|
||||||
}
|
|> ignoreValues
|
||||||
if description != (data.cachedData as? CachedChannelData)?.about {
|
|> mapError { _ in return Void() }
|
||||||
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
|
|
||||||
}
|
}
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
if description != (data.cachedData as? CachedChannelData)?.about {
|
||||||
}, completed: {
|
updateDataSignals.append(
|
||||||
guard let strongSelf = self else {
|
updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description)
|
||||||
return
|
|> 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 {
|
} else {
|
||||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||||
}
|
}
|
||||||
@ -1821,6 +1913,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateData(_ data: PeerInfoScreenData) {
|
private func updateData(_ data: PeerInfoScreenData) {
|
||||||
|
let previousData = self.data
|
||||||
var previousMemberCount: Int?
|
var previousMemberCount: Int?
|
||||||
if let data = self.data {
|
if let data = self.data {
|
||||||
if let members = data.members, case let .shortList(_, memberList) = members {
|
if let members = data.members, case let .shortList(_, memberList) = members {
|
||||||
@ -1828,6 +1921,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.data = data
|
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 {
|
if let (layout, navigationHeight) = self.validLayout {
|
||||||
var updatedMemberCount: Int?
|
var updatedMemberCount: Int?
|
||||||
if let data = self.data {
|
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) {
|
if user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in
|
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in
|
||||||
dismissAction()
|
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)
|
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() {
|
private func openDeletePeer() {
|
||||||
let peerId = self.peerId
|
let peerId = self.peerId
|
||||||
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
|
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 {
|
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)
|
self?.openPeer(peerId: peer.id, navigation: .info)
|
||||||
}, updateActivity: { _ in
|
}, updateActivity: { _ in
|
||||||
}, pushController: { [weak self] c in
|
}, pushController: { [weak self] c in
|
||||||
@ -3798,12 +3916,15 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
self.canAddVelocity = true
|
self.canAddVelocity = true
|
||||||
|
self.canOpenAvatarByDragging = self.headerNode.isAvatarExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
private var previousVelocityM1: CGFloat = 0.0
|
private var previousVelocityM1: CGFloat = 0.0
|
||||||
private var previousVelocity: CGFloat = 0.0
|
private var previousVelocity: CGFloat = 0.0
|
||||||
private var canAddVelocity: Bool = false
|
private var canAddVelocity: Bool = false
|
||||||
|
|
||||||
|
private var canOpenAvatarByDragging = false
|
||||||
|
|
||||||
private let velocityKey: String = encodeText("`wfsujdbmWfmpdjuz", -1)
|
private let velocityKey: String = encodeText("`wfsujdbmWfmpdjuz", -1)
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
@ -3825,9 +3946,15 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
if offsetY <= -32.0 && scrollView.isDragging && scrollView.isTracking {
|
if offsetY <= -32.0 && scrollView.isDragging && scrollView.isTracking {
|
||||||
if let peer = self.data?.peer, peer.smallProfileImage != nil {
|
if let peer = self.data?.peer, peer.smallProfileImage != nil {
|
||||||
shouldBeExpanded = true
|
shouldBeExpanded = true
|
||||||
|
|
||||||
|
if self.canOpenAvatarByDragging && self.headerNode.isAvatarExpanded && offsetY <= -32.0 {
|
||||||
|
self.canOpenAvatarByDragging = false
|
||||||
|
self.headerNode.initiateAvatarExpansion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if offsetY >= 1.0 {
|
} else if offsetY >= 1.0 {
|
||||||
shouldBeExpanded = false
|
shouldBeExpanded = false
|
||||||
|
self.canOpenAvatarByDragging = false
|
||||||
}
|
}
|
||||||
if let shouldBeExpanded = shouldBeExpanded, shouldBeExpanded != self.headerNode.isAvatarExpanded {
|
if let shouldBeExpanded = shouldBeExpanded, shouldBeExpanded != self.headerNode.isAvatarExpanded {
|
||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
||||||
|
@ -684,7 +684,7 @@ class WebSearchControllerNode: ASDisplayNode {
|
|||||||
var entries: [WebSearchGalleryEntry] = []
|
var entries: [WebSearchGalleryEntry] = []
|
||||||
var centralIndex: Int = 0
|
var centralIndex: Int = 0
|
||||||
for i in 0 ..< results.count {
|
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 {
|
if results[i] == currentResult {
|
||||||
centralIndex = i
|
centralIndex = i
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ final class WebSearchGalleryControllerInteraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct WebSearchGalleryEntry: Equatable {
|
struct WebSearchGalleryEntry: Equatable {
|
||||||
|
let index: Int
|
||||||
let result: ChatContextResult
|
let result: ChatContextResult
|
||||||
|
|
||||||
static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool {
|
static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool {
|
||||||
@ -39,11 +40,11 @@ struct WebSearchGalleryEntry: Equatable {
|
|||||||
case let .externalReference(_, _, type, _, _, _, content, thumbnail, _):
|
case let .externalReference(_, _, type, _, _, _, content, thumbnail, _):
|
||||||
if let content = content, type == "gif", let thumbnailResource = thumbnail?.resource, let dimensions = content.dimensions {
|
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: [])]))
|
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, _):
|
case let .internalReference(_, _, _, _, _, _, file, _):
|
||||||
if let file = 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()
|
preconditionFailure()
|
||||||
|
@ -14,15 +14,22 @@ import TelegramUniversalVideoContent
|
|||||||
import GalleryUI
|
import GalleryUI
|
||||||
|
|
||||||
class WebSearchVideoGalleryItem: GalleryItem {
|
class WebSearchVideoGalleryItem: GalleryItem {
|
||||||
|
var id: AnyHashable {
|
||||||
|
return self.index
|
||||||
|
}
|
||||||
|
|
||||||
|
let index: Int
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let presentationData: PresentationData
|
let presentationData: PresentationData
|
||||||
let result: ChatContextResult
|
let result: ChatContextResult
|
||||||
let content: UniversalVideoContent
|
let content: UniversalVideoContent
|
||||||
let controllerInteraction: WebSearchGalleryControllerInteraction?
|
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.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
|
self.index = index
|
||||||
self.result = result
|
self.result = result
|
||||||
self.content = content
|
self.content = content
|
||||||
self.controllerInteraction = controllerInteraction
|
self.controllerInteraction = controllerInteraction
|
||||||
|
Loading…
x
Reference in New Issue
Block a user