mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories
This commit is contained in:
parent
0f345717f7
commit
7c38aaf1cb
@ -1848,6 +1848,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
if let size = info.size {
|
if let size = info.size {
|
||||||
fetchRange = (0 ..< Int64(size), .default)
|
fetchRange = (0 ..< Int64(size), .default)
|
||||||
}
|
}
|
||||||
|
#if DEBUG
|
||||||
|
fetchRange = nil
|
||||||
|
#endif
|
||||||
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2516,6 +2519,51 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
self.push(storyContainerScreen)
|
self.push(storyContainerScreen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { [weak self] c, _ in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(self.navigationController as? NavigationController)?.pushViewController(controller)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
|
||||||
|
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||||
|
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -914,34 +914,30 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
|||||||
if itemNode.listNode.isTracking {
|
if itemNode.listNode.isTracking {
|
||||||
if case let .known(value) = offset {
|
if case let .known(value) = offset {
|
||||||
if !self.storiesUnlocked {
|
if !self.storiesUnlocked {
|
||||||
if value < -1.0 {
|
if value < -50.0 {
|
||||||
self.storiesUnlocked = true
|
self.storiesUnlocked = true
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HapticFeedback().impact()
|
||||||
|
|
||||||
self.currentItemNode.ignoreStoryInsetAdjustment = true
|
self.currentItemNode.ignoreStoryInsetAdjustment = true
|
||||||
|
self.currentItemNode.allowInsetFixWhileTracking = true
|
||||||
self.onStoriesLockedUpdated?(true)
|
self.onStoriesLockedUpdated?(true)
|
||||||
self.currentItemNode.ignoreStoryInsetAdjustment = false
|
self.currentItemNode.ignoreStoryInsetAdjustment = false
|
||||||
|
self.currentItemNode.allowInsetFixWhileTracking = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if self.storiesUnlocked {
|
||||||
switch offset {
|
switch offset {
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
if value >= 94.0 {
|
if value >= 94.0 {
|
||||||
if self.storiesUnlocked {
|
self.storiesUnlocked = false
|
||||||
self.storiesUnlocked = false
|
self.onStoriesLockedUpdated?(false)
|
||||||
self.currentItemNode.stopScrolling()
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.onStoriesLockedUpdated?(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -957,7 +953,6 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
|||||||
if value > 94.0 {
|
if value > 94.0 {
|
||||||
if self.storiesUnlocked {
|
if self.storiesUnlocked {
|
||||||
self.storiesUnlocked = false
|
self.storiesUnlocked = false
|
||||||
self.currentItemNode.stopScrolling()
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -1720,7 +1715,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.controller?.requestLayout(transition: .immediate)
|
//self.controller?.requestLayout(transition: .immediate)
|
||||||
|
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
}
|
}
|
||||||
|
|
||||||
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
|
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
|
||||||
|
@ -1214,6 +1214,8 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.useMainQueueTransactions = true
|
||||||
|
|
||||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||||
|
|
||||||
@ -3128,6 +3130,7 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var options = transition.options
|
var options = transition.options
|
||||||
|
options.insert(.Synchronous)
|
||||||
if self.view.window != nil {
|
if self.view.window != nil {
|
||||||
if !options.contains(.AnimateInsertion) {
|
if !options.contains(.AnimateInsertion) {
|
||||||
options.insert(.PreferSynchronousDrawing)
|
options.insert(.PreferSynchronousDrawing)
|
||||||
|
@ -212,6 +212,62 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setFrameWithAdditivePosition(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
assert(view.layer.anchorPoint == CGPoint())
|
||||||
|
|
||||||
|
if view.frame == frame {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedBounds: Bool?
|
||||||
|
var completedPosition: Bool?
|
||||||
|
let processCompletion: () -> Void = {
|
||||||
|
guard let completedBounds, let completedPosition else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion?(completedBounds && completedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setBounds(view: view, bounds: CGRect(origin: view.bounds.origin, size: frame.size), completion: { value in
|
||||||
|
completedBounds = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
self.animatePosition(view: view, from: CGPoint(x: -frame.minX + view.layer.position.x, y: -frame.minY + view.layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||||
|
completedPosition = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
view.layer.position = frame.origin
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setFrameWithAdditivePosition(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
assert(layer.anchorPoint == CGPoint())
|
||||||
|
|
||||||
|
if layer.frame == frame {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedBounds: Bool?
|
||||||
|
var completedPosition: Bool?
|
||||||
|
let processCompletion: () -> Void = {
|
||||||
|
guard let completedBounds, let completedPosition else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion?(completedBounds && completedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setBounds(layer: layer, bounds: CGRect(origin: layer.bounds.origin, size: frame.size), completion: { value in
|
||||||
|
completedBounds = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
self.animatePosition(layer: layer, from: CGPoint(x: -frame.minX + layer.position.x, y: -frame.minY + layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||||
|
completedPosition = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
layer.position = frame.origin
|
||||||
|
}
|
||||||
|
|
||||||
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
|
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
if view.bounds == bounds {
|
if view.bounds == bounds {
|
||||||
completion?(true)
|
completion?(true)
|
||||||
|
@ -206,6 +206,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
public final var dynamicBounceEnabled = true
|
public final var dynamicBounceEnabled = true
|
||||||
public final var rotated = false
|
public final var rotated = false
|
||||||
public final var experimentalSnapScrollToItem = false
|
public final var experimentalSnapScrollToItem = false
|
||||||
|
public final var useMainQueueTransactions = false
|
||||||
|
|
||||||
public final var scrollEnabled: Bool = true {
|
public final var scrollEnabled: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
@ -250,6 +251,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public final var snapToBottomInsetUntilFirstInteraction: Bool = false
|
public final var snapToBottomInsetUntilFirstInteraction: Bool = false
|
||||||
|
public final var allowInsetFixWhileTracking: Bool = false
|
||||||
|
|
||||||
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||||
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
||||||
@ -595,7 +597,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
DispatchQueue.main.async(execute: action)
|
if self.useMainQueueTransactions && Thread.isMainThread {
|
||||||
|
action()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async(execute: action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func beginReordering(itemNode: ListViewItemNode) {
|
private func beginReordering(itemNode: ListViewItemNode) {
|
||||||
@ -980,7 +986,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
self.trackingOffset += -deltaY
|
self.trackingOffset += -deltaY
|
||||||
}
|
}
|
||||||
|
|
||||||
self.enqueueUpdateVisibleItems(synchronous: false)
|
if self.useMainQueueTransactions {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.enqueueUpdateVisibleItems(synchronous: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.enqueueUpdateVisibleItems(synchronous: false)
|
||||||
|
}
|
||||||
|
|
||||||
var useScrollDynamics = false
|
var useScrollDynamics = false
|
||||||
|
|
||||||
@ -1630,19 +1642,29 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
|
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
|
||||||
self.ignoreScrollingEvents = true
|
self.ignoreScrollingEvents = true
|
||||||
if topItemFound && bottomItemFound {
|
if topItemFound && bottomItemFound {
|
||||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
|
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||||
|
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
|
||||||
|
}
|
||||||
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
||||||
self.scroller.contentOffset = self.lastContentOffset
|
if self.scroller.contentOffset != self.lastContentOffset {
|
||||||
|
self.scroller.contentOffset = self.lastContentOffset
|
||||||
|
}
|
||||||
} else if topItemFound {
|
} else if topItemFound {
|
||||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||||
|
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||||
|
}
|
||||||
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
||||||
if self.scroller.contentOffset != self.lastContentOffset {
|
if self.scroller.contentOffset != self.lastContentOffset {
|
||||||
self.scroller.contentOffset = self.lastContentOffset
|
self.scroller.contentOffset = self.lastContentOffset
|
||||||
}
|
}
|
||||||
} else if bottomItemFound {
|
} else if bottomItemFound {
|
||||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||||
|
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||||
|
}
|
||||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
|
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
|
||||||
self.scroller.contentOffset = self.lastContentOffset
|
if self.scroller.contentOffset != self.lastContentOffset {
|
||||||
|
self.scroller.contentOffset = self.lastContentOffset
|
||||||
|
}
|
||||||
} else if self.itemNodes.isEmpty {
|
} else if self.itemNodes.isEmpty {
|
||||||
self.scroller.contentSize = self.visibleSize
|
self.scroller.contentSize = self.visibleSize
|
||||||
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
|
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
|
||||||
@ -1650,10 +1672,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
self.lastContentOffset = .zero
|
self.lastContentOffset = .zero
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||||
|
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||||
|
}
|
||||||
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
|
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
|
||||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
|
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
|
||||||
self.scroller.contentOffset = self.lastContentOffset
|
if self.scroller.contentOffset != self.lastContentOffset {
|
||||||
|
self.scroller.contentOffset = self.lastContentOffset
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.lastContentOffset = self.scroller.contentOffset
|
self.lastContentOffset = self.scroller.contentOffset
|
||||||
}
|
}
|
||||||
@ -1662,8 +1688,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func async(_ f: @escaping () -> Void) {
|
private func async(_ f: @escaping () -> Void) {
|
||||||
DispatchQueue.global(qos: .userInteractive).async(execute: f)
|
if self.useMainQueueTransactions {
|
||||||
//DispatchQueue.main.async(execute: f)
|
if Thread.isMainThread {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async(execute: f)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.global(qos: .userInteractive).async(execute: f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
@ -2951,7 +2984,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
|
|
||||||
var offsetFix: CGFloat
|
var offsetFix: CGFloat
|
||||||
let insetDeltaOffsetFix: CGFloat = 0.0
|
let insetDeltaOffsetFix: CGFloat = 0.0
|
||||||
if self.isTracking || isExperimentalSnapToScrollToItem {
|
if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem {
|
||||||
offsetFix = 0.0
|
offsetFix = 0.0
|
||||||
} else if self.snapToBottomInsetUntilFirstInteraction {
|
} else if self.snapToBottomInsetUntilFirstInteraction {
|
||||||
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom
|
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom
|
||||||
|
@ -1450,7 +1450,7 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
if let titleView = titleView as? NavigationBarTitleView {
|
if let titleView = titleView as? NavigationBarTitleView {
|
||||||
let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset)
|
let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset)
|
||||||
|
|
||||||
let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), sideContentWidth: 0.0, transition: titleViewTransition)
|
let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {
|
if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {
|
||||||
|
@ -4,5 +4,5 @@ import UIKit
|
|||||||
public protocol NavigationBarTitleView {
|
public protocol NavigationBarTitleView {
|
||||||
func animateLayoutTransition()
|
func animateLayoutTransition()
|
||||||
|
|
||||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat
|
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
|
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||||
let leftInset: CGFloat = 0.0
|
let leftInset: CGFloat = 0.0
|
||||||
let rightInset: CGFloat = 0.0
|
let rightInset: CGFloat = 0.0
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
|
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.0
|
return CGRect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateLayoutTransition() {
|
func animateLayoutTransition() {
|
||||||
|
@ -636,7 +636,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
|||||||
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
|
||||||
if let (size, clearBounds) = self.validLayout {
|
if let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate)
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,11 +644,11 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
|||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
if let (size, clearBounds) = self.validLayout {
|
if let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate)
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||||
self.validLayout = (size, clearBounds)
|
self.validLayout = (size, clearBounds)
|
||||||
|
|
||||||
let titleSize = self.titleNode.updateLayout(size)
|
let titleSize = self.titleNode.updateLayout(size)
|
||||||
@ -661,7 +661,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
|||||||
self.titleNode.frame = titleFrame
|
self.titleNode.frame = titleFrame
|
||||||
self.subtitleNode.frame = subtitleFrame
|
self.subtitleNode.frame = subtitleFrame
|
||||||
|
|
||||||
return titleSize.width
|
return titleFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateLayoutTransition() {
|
func animateLayoutTransition() {
|
||||||
|
@ -1036,3 +1036,224 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class EngineStoryViewListContext {
|
||||||
|
public struct LoadMoreToken: Equatable {
|
||||||
|
var id: Int64
|
||||||
|
var timestamp: Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class Item: Equatable {
|
||||||
|
public let peer: EnginePeer
|
||||||
|
public let timestamp: Int32
|
||||||
|
|
||||||
|
public init(
|
||||||
|
peer: EnginePeer,
|
||||||
|
timestamp: Int32
|
||||||
|
) {
|
||||||
|
self.peer = peer
|
||||||
|
self.timestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.timestamp != rhs.timestamp {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct State: Equatable {
|
||||||
|
public var totalCount: Int
|
||||||
|
public var items: [Item]
|
||||||
|
public var loadMoreToken: LoadMoreToken?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
totalCount: Int,
|
||||||
|
items: [Item],
|
||||||
|
loadMoreToken: LoadMoreToken?
|
||||||
|
) {
|
||||||
|
self.totalCount = totalCount
|
||||||
|
self.items = items
|
||||||
|
self.loadMoreToken = loadMoreToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Impl {
|
||||||
|
struct NextOffset: Equatable {
|
||||||
|
var id: Int64
|
||||||
|
var timestamp: Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InternalState: Equatable {
|
||||||
|
var totalCount: Int
|
||||||
|
var items: [Item]
|
||||||
|
var canLoadMore: Bool
|
||||||
|
var nextOffset: NextOffset?
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue: Queue
|
||||||
|
|
||||||
|
let account: Account
|
||||||
|
let storyId: Int32
|
||||||
|
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
var state: InternalState
|
||||||
|
let statePromise = Promise<InternalState>()
|
||||||
|
|
||||||
|
var isLoadingMore: Bool = false
|
||||||
|
|
||||||
|
init(queue: Queue, account: Account, storyId: Int32, views: EngineStoryItem.Views) {
|
||||||
|
self.queue = queue
|
||||||
|
self.account = account
|
||||||
|
self.storyId = storyId
|
||||||
|
|
||||||
|
let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0))
|
||||||
|
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
|
||||||
|
self.statePromise.set(.single(self.state))
|
||||||
|
|
||||||
|
if initialState.loadMoreToken != nil {
|
||||||
|
self.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
|
self.disposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMore() {
|
||||||
|
if self.isLoadingMore {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isLoadingMore = true
|
||||||
|
|
||||||
|
let account = self.account
|
||||||
|
let storyId = self.storyId
|
||||||
|
let currentOffset = self.state.nextOffset
|
||||||
|
let limit = self.state.items.isEmpty ? 50 : 100
|
||||||
|
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Void in
|
||||||
|
}
|
||||||
|
|> mapToSignal { _ -> Signal<InternalState, NoError> in
|
||||||
|
return account.network.request(Api.functions.stories.getStoryViewsList(id: storyId, offsetDate: currentOffset?.timestamp ?? 0, offsetId: currentOffset?.id ?? 0, limit: Int32(limit)))
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<InternalState, NoError> in
|
||||||
|
return account.postbox.transaction { transaction -> InternalState in
|
||||||
|
switch result {
|
||||||
|
case let .storyViewsList(count, views, users):
|
||||||
|
var peers: [Peer] = []
|
||||||
|
var peerPresences: [PeerId: Api.User] = [:]
|
||||||
|
|
||||||
|
for user in users {
|
||||||
|
let telegramUser = TelegramUser(user: user)
|
||||||
|
peers.append(telegramUser)
|
||||||
|
peerPresences[telegramUser.id] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||||
|
|
||||||
|
var items: [Item] = []
|
||||||
|
var nextOffset: NextOffset?
|
||||||
|
for view in views {
|
||||||
|
switch view {
|
||||||
|
case let .storyView(userId, date):
|
||||||
|
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
|
||||||
|
items.append(Item(peer: EnginePeer(peer), timestamp: date))
|
||||||
|
|
||||||
|
nextOffset = NextOffset(id: userId, timestamp: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
|
||||||
|
case .none:
|
||||||
|
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.disposable.set((signal
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] state in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ItemHash: Hashable {
|
||||||
|
var peerId: EnginePeer.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingItems = Set<ItemHash>()
|
||||||
|
for item in strongSelf.state.items {
|
||||||
|
existingItems.insert(ItemHash(peerId: item.peer.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in state.items {
|
||||||
|
let itemHash = ItemHash(peerId: item.peer.id)
|
||||||
|
if existingItems.contains(itemHash) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingItems.insert(itemHash)
|
||||||
|
strongSelf.state.items.append(item)
|
||||||
|
}
|
||||||
|
if state.canLoadMore {
|
||||||
|
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
|
||||||
|
} else {
|
||||||
|
strongSelf.state.totalCount = strongSelf.state.items.count
|
||||||
|
}
|
||||||
|
strongSelf.state.canLoadMore = state.canLoadMore
|
||||||
|
strongSelf.state.nextOffset = state.nextOffset
|
||||||
|
|
||||||
|
strongSelf.isLoadingMore = false
|
||||||
|
strongSelf.statePromise.set(.single(strongSelf.state))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue: Queue
|
||||||
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
|
||||||
|
public var state: Signal<State, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.statePromise.get().start(next: { state in
|
||||||
|
var loadMoreToken: LoadMoreToken?
|
||||||
|
if let nextOffset = state.nextOffset {
|
||||||
|
loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp)
|
||||||
|
}
|
||||||
|
subscriber.putNext(State(
|
||||||
|
totalCount: state.totalCount,
|
||||||
|
items: state.items,
|
||||||
|
loadMoreToken: loadMoreToken
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(account: Account, storyId: Int32, views: EngineStoryItem.Views) {
|
||||||
|
let queue = Queue()
|
||||||
|
self.queue = queue
|
||||||
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||||
|
return Impl(queue: queue, account: account, storyId: storyId, views: views)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadMore() {
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -878,5 +878,9 @@ public extension TelegramEngine {
|
|||||||
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
||||||
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
|
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext {
|
||||||
|
return EngineStoryViewListContext(account: self.account, storyId: id, views: views)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,6 +293,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
|
|
||||||
var contentOffsetFraction: CGFloat = 0.0
|
var contentOffsetFraction: CGFloat = 0.0
|
||||||
private(set) var centerContentWidth: CGFloat = 0.0
|
private(set) var centerContentWidth: CGFloat = 0.0
|
||||||
|
private(set) var centerContentOffsetX: CGFloat = 0.0
|
||||||
|
|
||||||
init(
|
init(
|
||||||
backPressed: @escaping () -> Void,
|
backPressed: @escaping () -> Void,
|
||||||
@ -440,7 +441,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, size: CGSize, transition: Transition) {
|
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: Transition) {
|
||||||
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
|
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
|
||||||
|
|
||||||
@ -616,6 +617,8 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var centerContentWidth: CGFloat = 0.0
|
||||||
|
var centerContentOffsetX: CGFloat = 0.0
|
||||||
if let chatListTitle = content.chatListTitle {
|
if let chatListTitle = content.chatListTitle {
|
||||||
var chatListTitleTransition = transition
|
var chatListTitleTransition = transition
|
||||||
let chatListTitleView: ChatListTitleView
|
let chatListTitleView: ChatListTitleView
|
||||||
@ -633,8 +636,13 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
chatListTitleView.theme = theme
|
chatListTitleView.theme = theme
|
||||||
chatListTitleView.strings = strings
|
chatListTitleView.strings = strings
|
||||||
chatListTitleView.setTitle(chatListTitle, animated: false)
|
chatListTitleView.setTitle(chatListTitle, animated: false)
|
||||||
let centerContentWidth = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), sideContentWidth: sideContentWidth, transition: transition.containedViewLayoutTransition)
|
let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
|
||||||
self.centerContentWidth = centerContentWidth
|
centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0)
|
||||||
|
|
||||||
|
//sideWidth + centerWidth + centerOffset = size.width
|
||||||
|
//let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5
|
||||||
|
let centerOffset = sideContentWidth
|
||||||
|
centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset)
|
||||||
|
|
||||||
chatListTitleView.openStatusSetup = { [weak self] sourceView in
|
chatListTitleView.openStatusSetup = { [weak self] sourceView in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -649,7 +657,14 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
self.toggleIsLocked()
|
self.toggleIsLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
|
let chatListTitleOffset: CGFloat
|
||||||
|
if chatListTitle.activity {
|
||||||
|
chatListTitleOffset = 0.0
|
||||||
|
} else {
|
||||||
|
chatListTitleOffset = (centerOffset + centerContentOffsetX) * sideContentFraction
|
||||||
|
}
|
||||||
|
|
||||||
|
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: chatListTitleOffset + floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
|
||||||
} else {
|
} else {
|
||||||
if let chatListTitleView = self.chatListTitleView {
|
if let chatListTitleView = self.chatListTitleView {
|
||||||
self.chatListTitleView = nil
|
self.chatListTitleView = nil
|
||||||
@ -658,6 +673,8 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil
|
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil
|
||||||
|
self.centerContentWidth = centerContentWidth
|
||||||
|
self.centerContentOffsetX = centerContentOffsetX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,6 +689,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
private let storyPeerListExternalState = StoryPeerListComponent.ExternalState()
|
private let storyPeerListExternalState = StoryPeerListComponent.ExternalState()
|
||||||
private var storyPeerList: ComponentView<Empty>?
|
private var storyPeerList: ComponentView<Empty>?
|
||||||
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
||||||
|
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
|
||||||
|
|
||||||
private var effectiveContentView: ContentView? {
|
private var effectiveContentView: ContentView? {
|
||||||
return self.secondaryContentView ?? self.primaryContentView
|
return self.secondaryContentView ?? self.primaryContentView
|
||||||
@ -803,10 +821,16 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.storyPeerAction?(peer)
|
self.storyPeerAction?(peer)
|
||||||
|
},
|
||||||
|
contextPeerAction: { [weak self] sourceNode, gesture, peer in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.storyContextPeerAction?(sourceNode, gesture, peer)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: self.bounds.width, height: 94.0)
|
containerSize: CGSize(width: availableSize.width, height: 94.0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,7 +875,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth * (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
|
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
|
||||||
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
|
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
|
||||||
@ -890,7 +914,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
self.secondaryContentView = secondaryContentView
|
self.secondaryContentView = secondaryContentView
|
||||||
self.addSubview(secondaryContentView)
|
self.addSubview(secondaryContentView)
|
||||||
}
|
}
|
||||||
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, size: availableSize, transition: secondaryContentTransition)
|
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition)
|
||||||
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
|
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
|
||||||
@ -946,7 +970,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
|
|
||||||
var defaultStoryListX: CGFloat = 0.0
|
var defaultStoryListX: CGFloat = 0.0
|
||||||
if let primaryContentView = self.primaryContentView {
|
if let primaryContentView = self.primaryContentView {
|
||||||
defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5)
|
defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5) + primaryContentView.centerContentOffsetX
|
||||||
}
|
}
|
||||||
|
|
||||||
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 94.0)))
|
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 94.0)))
|
||||||
|
@ -137,7 +137,9 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
override public init(frame: CGRect) {
|
override public init(frame: CGRect) {
|
||||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
|
||||||
self.separatorLayer = SimpleLayer()
|
self.separatorLayer = SimpleLayer()
|
||||||
|
self.separatorLayer.anchorPoint = CGPoint()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
@ -167,10 +169,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func applyScroll(offset: CGFloat, transition: Transition) {
|
public func applyScroll(offset: CGFloat, transition: Transition) {
|
||||||
var transition = transition
|
let transition = transition
|
||||||
if self.applyScrollFractionAnimator != nil {
|
|
||||||
transition = .immediate
|
|
||||||
}
|
|
||||||
|
|
||||||
self.rawScrollOffset = offset
|
self.rawScrollOffset = offset
|
||||||
|
|
||||||
@ -211,9 +210,13 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
let previousHeight = self.backgroundView.bounds.height
|
let previousHeight = self.backgroundView.bounds.height
|
||||||
|
|
||||||
self.backgroundView.update(size: visibleSize, transition: transition.containedViewLayoutTransition)
|
self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition)
|
||||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: visibleSize))
|
|
||||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel)))
|
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0)))
|
||||||
|
transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true)
|
||||||
|
self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)
|
||||||
|
|
||||||
|
transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
let searchContentNode: NavigationBarSearchContentNode
|
let searchContentNode: NavigationBarSearchContentNode
|
||||||
if let current = self.searchContentNode {
|
if let current = self.searchContentNode {
|
||||||
@ -247,6 +250,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
component.activateSearch(searchContentNode)
|
component.activateSearch(searchContentNode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
searchContentNode.view.layer.anchorPoint = CGPoint()
|
||||||
self.searchContentNode = searchContentNode
|
self.searchContentNode = searchContentNode
|
||||||
self.addSubview(searchContentNode.view)
|
self.addSubview(searchContentNode.view)
|
||||||
}
|
}
|
||||||
@ -273,11 +277,17 @@ public final class ChatListNavigationBar: Component {
|
|||||||
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
|
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
|
||||||
searchContentNode.expansionProgress = 1.0 - searchOffsetFraction
|
searchContentNode.expansionProgress = 1.0 - searchOffsetFraction
|
||||||
|
|
||||||
transition.setFrame(view: searchContentNode.view, frame: searchFrame)
|
transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame)
|
||||||
|
|
||||||
searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition)
|
searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
var headerTransition = transition
|
||||||
|
if self.applyScrollFractionAnimator != nil {
|
||||||
|
headerTransition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
let headerContentSize = self.headerContent.update(
|
let headerContentSize = self.headerContent.update(
|
||||||
transition: transition,
|
transition: headerTransition,
|
||||||
component: AnyComponent(ChatListHeaderComponent(
|
component: AnyComponent(ChatListHeaderComponent(
|
||||||
sideInset: component.sideInset + 16.0,
|
sideInset: component.sideInset + 16.0,
|
||||||
primaryContent: component.primaryContent,
|
primaryContent: component.primaryContent,
|
||||||
@ -318,9 +328,10 @@ public final class ChatListNavigationBar: Component {
|
|||||||
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
||||||
if let headerContentView = self.headerContent.view {
|
if let headerContentView = self.headerContent.view {
|
||||||
if headerContentView.superview == nil {
|
if headerContentView.superview == nil {
|
||||||
|
headerContentView.layer.anchorPoint = CGPoint()
|
||||||
self.addSubview(headerContentView)
|
self.addSubview(headerContentView)
|
||||||
}
|
}
|
||||||
transition.setFrame(view: headerContentView, frame: headerContentFrame)
|
transition.setFrameWithAdditivePosition(view: headerContentView, frame: headerContentFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
if component.tabsNode !== self.tabsNode {
|
if component.tabsNode !== self.tabsNode {
|
||||||
@ -342,7 +353,8 @@ public final class ChatListNavigationBar: Component {
|
|||||||
let tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 46.0), size: CGSize(width: visibleSize.width, height: 46.0))
|
let tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 46.0), size: CGSize(width: visibleSize.width, height: 46.0))
|
||||||
|
|
||||||
if let disappearingTabsView = self.disappearingTabsView {
|
if let disappearingTabsView = self.disappearingTabsView {
|
||||||
transition.setFrame(view: disappearingTabsView, frame: tabsFrame)
|
disappearingTabsView.layer.anchorPoint = CGPoint()
|
||||||
|
transition.setFrameWithAdditivePosition(view: disappearingTabsView, frame: tabsFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let tabsNode = component.tabsNode {
|
if let tabsNode = component.tabsNode {
|
||||||
@ -350,6 +362,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
var tabsNodeTransition = transition
|
var tabsNodeTransition = transition
|
||||||
if tabsNode.view.superview !== self {
|
if tabsNode.view.superview !== self {
|
||||||
|
tabsNode.view.layer.anchorPoint = CGPoint()
|
||||||
tabsNodeTransition = .immediate
|
tabsNodeTransition = .immediate
|
||||||
self.addSubview(tabsNode.view)
|
self.addSubview(tabsNode.view)
|
||||||
if !transition.animation.isImmediate {
|
if !transition.animation.isImmediate {
|
||||||
@ -359,7 +372,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabsNodeTransition.setFrame(view: tabsNode.view, frame: tabsFrame)
|
tabsNodeTransition.setFrameWithAdditivePosition(view: tabsNode.view, frame: tabsFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
|||||||
|
|
||||||
public var openStatusSetup: ((UIView) -> Void)?
|
public var openStatusSetup: ((UIView) -> Void)?
|
||||||
|
|
||||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
private var validLayout: (CGSize, CGRect)?
|
||||||
|
|
||||||
public var manualLayout: Bool = false
|
public var manualLayout: Bool = false
|
||||||
|
|
||||||
@ -316,13 +316,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
|||||||
override public func layoutSubviews() {
|
override public func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
self.validLayout = (size, clearBounds)
|
||||||
|
|
||||||
var indicatorPadding: CGFloat = 0.0
|
var indicatorPadding: CGFloat = 0.0
|
||||||
let indicatorSize = self.activityIndicator.bounds.size
|
let indicatorSize = self.activityIndicator.bounds.size
|
||||||
@ -344,9 +344,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
|||||||
|
|
||||||
let combinedHeight = titleSize.height
|
let combinedHeight = titleSize.height
|
||||||
|
|
||||||
let combinedWidth = sideContentWidth + titleSize.width
|
let combinedWidth = titleSize.width
|
||||||
|
|
||||||
var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0) + sideContentWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||||
|
|
||||||
titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width)
|
titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width)
|
||||||
|
|
||||||
@ -429,7 +429,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return combinedWidth
|
var resultFrame = titleFrame
|
||||||
|
if !self.lockView.isHidden {
|
||||||
|
resultFrame = resultFrame.union(lockFrame)
|
||||||
|
}
|
||||||
|
if let titleCredibilityIconView = self.titleCredibilityIconView {
|
||||||
|
resultFrame = resultFrame.union(titleCredibilityIconView.frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func buttonPressed() {
|
@objc private func buttonPressed() {
|
||||||
|
@ -118,7 +118,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
private let button: HighlightTrackingButtonNode
|
private let button: HighlightTrackingButtonNode
|
||||||
|
|
||||||
var manualLayout: Bool = false
|
var manualLayout: Bool = false
|
||||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
private var validLayout: (CGSize, CGRect)?
|
||||||
|
|
||||||
private var titleLeftIcon: ChatTitleIcon = .none
|
private var titleLeftIcon: ChatTitleIcon = .none
|
||||||
private var titleRightIcon: ChatTitleIcon = .none
|
private var titleRightIcon: ChatTitleIcon = .none
|
||||||
@ -355,8 +355,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.button.isUserInteractionEnabled = isEnabled
|
self.button.isUserInteractionEnabled = isEnabled
|
||||||
if !self.updateStatus() {
|
if !self.updateStatus() {
|
||||||
if updated {
|
if updated {
|
||||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.2, curve: .easeInOut))
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -605,8 +605,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.activityNode.transitionToState(state, animation: .slide) {
|
if self.activityNode.transitionToState(state, animation: .slide) {
|
||||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.3, curve: .spring))
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@ -688,8 +688,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
override public func layoutSubviews() {
|
override public func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -704,14 +704,14 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.titleContent = titleContent
|
self.titleContent = titleContent
|
||||||
let _ = self.updateStatus()
|
let _ = self.updateStatus()
|
||||||
|
|
||||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
self.validLayout = (size, clearBounds)
|
||||||
|
|
||||||
self.button.frame = clearBounds
|
self.button.frame = clearBounds
|
||||||
self.contentContainer.frame = clearBounds
|
self.contentContainer.frame = clearBounds
|
||||||
@ -851,7 +851,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
|
|
||||||
self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0)))
|
self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0)))
|
||||||
|
|
||||||
return titleFrame.width
|
return titleFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func buttonPressed() {
|
@objc private func buttonPressed() {
|
||||||
@ -1015,7 +1015,7 @@ public final class ChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false)
|
contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false)
|
||||||
|
|
||||||
let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), sideContentWidth: 0.0, transition: transition.containedViewLayoutTransition)
|
let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), transition: transition.containedViewLayoutTransition)
|
||||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
|
@ -51,6 +51,8 @@ swift_library(
|
|||||||
"//submodules/ContextUI",
|
"//submodules/ContextUI",
|
||||||
"//submodules/AvatarNode",
|
"//submodules/AvatarNode",
|
||||||
"//submodules/ChatPresentationInterfaceState",
|
"//submodules/ChatPresentationInterfaceState",
|
||||||
|
"//submodules/TelegramStringFormatting",
|
||||||
|
"//submodules/ShimmerEffect",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -0,0 +1,360 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import MultilineTextComponent
|
||||||
|
import AvatarNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
import CheckNode
|
||||||
|
import TelegramStringFormatting
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||||
|
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
|
||||||
|
final class PeerListItemComponent: Component {
|
||||||
|
final class TransitionHint {
|
||||||
|
let synchronousLoad: Bool
|
||||||
|
|
||||||
|
init(synchronousLoad: Bool) {
|
||||||
|
self.synchronousLoad = synchronousLoad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SelectionState: Equatable {
|
||||||
|
case none
|
||||||
|
case editing(isSelected: Bool, isTinted: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let title: String
|
||||||
|
let peer: EnginePeer?
|
||||||
|
let subtitle: String?
|
||||||
|
let selectionState: SelectionState
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: (EnginePeer) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
title: String,
|
||||||
|
peer: EnginePeer?,
|
||||||
|
subtitle: String?,
|
||||||
|
selectionState: SelectionState,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping (EnginePeer) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.title = title
|
||||||
|
self.peer = peer
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.selectionState = selectionState
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subtitle != rhs.subtitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.selectionState != rhs.selectionState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
private let avatarNode: AvatarNode
|
||||||
|
|
||||||
|
private var iconView: UIImageView?
|
||||||
|
private var checkLayer: CheckLayer?
|
||||||
|
|
||||||
|
private var component: PeerListItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
var avatarFrame: CGRect {
|
||||||
|
return self.avatarNode.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleFrame: CGRect? {
|
||||||
|
return self.title.view?.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelFrame: CGRect? {
|
||||||
|
guard var value = self.label.view?.frame else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if let iconView = self.iconView {
|
||||||
|
value.size.width += value.minX - iconView.frame.minX
|
||||||
|
value.origin.x = iconView.frame.minX
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.containerButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.avatarNode = AvatarNode(font: avatarFont)
|
||||||
|
self.avatarNode.isLayerBacked = true
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
self.addSubview(self.containerButton)
|
||||||
|
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||||
|
|
||||||
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component, let peer = component.peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
var synchronousLoad = false
|
||||||
|
if let hint = transition.userData(TransitionHint.self) {
|
||||||
|
synchronousLoad = hint.synchronousLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
var hasSelectionUpdated = false
|
||||||
|
if let previousComponent = self.component {
|
||||||
|
switch previousComponent.selectionState {
|
||||||
|
case .none:
|
||||||
|
if case .none = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
case .editing:
|
||||||
|
if case .editing = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contextInset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let height: CGFloat = 60.0
|
||||||
|
let verticalInset: CGFloat = 1.0
|
||||||
|
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||||
|
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||||
|
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||||
|
|
||||||
|
if case let .editing(isSelected, isTinted) = component.selectionState {
|
||||||
|
leftInset += 44.0
|
||||||
|
avatarLeftInset += 44.0
|
||||||
|
let checkSize: CGFloat = 22.0
|
||||||
|
|
||||||
|
let checkLayer: CheckLayer
|
||||||
|
if let current = self.checkLayer {
|
||||||
|
checkLayer = current
|
||||||
|
if themeUpdated {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||||
|
}
|
||||||
|
checkLayer.theme = theme
|
||||||
|
}
|
||||||
|
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||||
|
} else {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||||
|
}
|
||||||
|
checkLayer = CheckLayer(theme: theme)
|
||||||
|
self.checkLayer = checkLayer
|
||||||
|
self.containerButton.layer.addSublayer(checkLayer)
|
||||||
|
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||||
|
checkLayer.setSelected(isSelected, animated: false)
|
||||||
|
checkLayer.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||||
|
} else {
|
||||||
|
if let checkLayer = self.checkLayer {
|
||||||
|
self.checkLayer = nil
|
||||||
|
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||||
|
checkLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize: CGFloat = 40.0
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
if self.avatarNode.bounds.isEmpty {
|
||||||
|
self.avatarNode.frame = avatarFrame
|
||||||
|
} else {
|
||||||
|
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||||
|
}
|
||||||
|
if let peer = component.peer {
|
||||||
|
let clipStyle: AvatarNodeClipStyle
|
||||||
|
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||||
|
clipStyle = .roundedRect
|
||||||
|
} else {
|
||||||
|
clipStyle = .round
|
||||||
|
}
|
||||||
|
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelData: (String, Bool)
|
||||||
|
if let subtitle = component.subtitle {
|
||||||
|
labelData = (subtitle, false)
|
||||||
|
} else if case .legacyGroup = component.peer {
|
||||||
|
labelData = (component.strings.Group_Status, false)
|
||||||
|
} else if case let .channel(channel) = component.peer {
|
||||||
|
if case .group = channel.info {
|
||||||
|
labelData = (component.strings.Group_Status, false)
|
||||||
|
} else {
|
||||||
|
labelData = (component.strings.Channel_Status, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelData = (component.strings.Group_Status, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let previousTitleFrame = self.title.view?.frame
|
||||||
|
var previousTitleContents: UIView?
|
||||||
|
if hasSelectionUpdated && !"".isEmpty {
|
||||||
|
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat = 1.0
|
||||||
|
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||||
|
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||||
|
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||||
|
self.addSubview(previousTitleContents)
|
||||||
|
|
||||||
|
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||||
|
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||||
|
previousTitleContents?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
var iconLabelOffset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let iconView: UIImageView
|
||||||
|
if let current = self.iconView {
|
||||||
|
iconView = current
|
||||||
|
} else {
|
||||||
|
iconView = UIImageView(image: readIconImage)
|
||||||
|
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||||
|
self.iconView = iconView
|
||||||
|
self.containerButton.addSubview(iconView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = iconView.image {
|
||||||
|
iconLabelOffset = image.size.width + 4.0
|
||||||
|
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -159,6 +159,14 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ViewList {
|
||||||
|
let externalState = StoryItemSetViewListComponent.ExternalState()
|
||||||
|
let view = ComponentView<Empty>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||||
let sendMessageContext: StoryItemSetContainerSendMessage
|
let sendMessageContext: StoryItemSetContainerSendMessage
|
||||||
|
|
||||||
@ -184,6 +192,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let footerPanel = ComponentView<Empty>()
|
let footerPanel = ComponentView<Empty>()
|
||||||
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||||
|
|
||||||
|
var displayViewList: Bool = false
|
||||||
|
var viewList: ViewList?
|
||||||
|
|
||||||
var itemLayout: ItemLayout?
|
var itemLayout: ItemLayout?
|
||||||
var ignoreScrolling: Bool = false
|
var ignoreScrolling: Bool = false
|
||||||
|
|
||||||
@ -388,6 +399,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
} else if self.displayReactions {
|
} else if self.displayReactions {
|
||||||
self.displayReactions = false
|
self.displayReactions = false
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
} else if self.displayViewList {
|
||||||
|
self.displayViewList = false
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
} else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 {
|
} else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 {
|
||||||
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
||||||
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
@ -485,7 +499,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
|
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
|
||||||
|
|
||||||
if let view = view as? StoryContentItem.View {
|
if let view = view as? StoryContentItem.View {
|
||||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
|
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,7 +524,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
for (_, visibleItem) in self.visibleItems {
|
for (_, visibleItem) in self.visibleItems {
|
||||||
if let view = visibleItem.view.view {
|
if let view = visibleItem.view.view {
|
||||||
if let view = view as? StoryContentItem.View {
|
if let view = view as? StoryContentItem.View {
|
||||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
|
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -869,6 +883,16 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
component: AnyComponent(StoryFooterPanelComponent(
|
component: AnyComponent(StoryFooterPanelComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
storyItem: currentItem?.storyItem,
|
storyItem: currentItem?.storyItem,
|
||||||
|
expandViewStats: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.displayViewList {
|
||||||
|
self.displayViewList = true
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
}
|
||||||
|
},
|
||||||
deleteAction: { [weak self] in
|
deleteAction: { [weak self] in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
@ -1053,8 +1077,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let bottomContentInsetWithoutInput = bottomContentInset
|
let bottomContentInsetWithoutInput = bottomContentInset
|
||||||
|
var viewListInset: CGFloat = 0.0
|
||||||
|
|
||||||
let inputPanelBottomInset: CGFloat
|
var inputPanelBottomInset: CGFloat
|
||||||
let inputPanelIsOverlay: Bool
|
let inputPanelIsOverlay: Bool
|
||||||
if component.inputHeight == 0.0 {
|
if component.inputHeight == 0.0 {
|
||||||
inputPanelBottomInset = bottomContentInset
|
inputPanelBottomInset = bottomContentInset
|
||||||
@ -1066,9 +1091,81 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
inputPanelIsOverlay = true
|
inputPanelIsOverlay = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - bottomContentInset))
|
if self.displayViewList {
|
||||||
transition.setFrame(view: self.contentContainerView, frame: contentFrame)
|
let viewList: ViewList
|
||||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0)
|
var viewListTransition = transition
|
||||||
|
if let current = self.viewList {
|
||||||
|
viewList = current
|
||||||
|
} else {
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
viewListTransition = .immediate
|
||||||
|
}
|
||||||
|
viewList = ViewList()
|
||||||
|
self.viewList = viewList
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewListSize = viewList.view.update(
|
||||||
|
transition: viewListTransition,
|
||||||
|
component: AnyComponent(StoryItemSetViewListComponent(
|
||||||
|
externalState: viewList.externalState,
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
safeInsets: component.safeInsets,
|
||||||
|
storyItem: component.slice.item.storyItem,
|
||||||
|
close: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.displayViewList = false
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: availableSize
|
||||||
|
)
|
||||||
|
let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize)
|
||||||
|
if let viewListView = viewList.view.view {
|
||||||
|
var animateIn = false
|
||||||
|
if viewListView.superview == nil {
|
||||||
|
self.addSubview(viewListView)
|
||||||
|
animateIn = true
|
||||||
|
}
|
||||||
|
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
|
||||||
|
|
||||||
|
if animateIn, !transition.animation.isImmediate {
|
||||||
|
transition.animatePosition(view: viewListView, from: CGPoint(x: 0.0, y: viewListFrame.height), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewListInset = viewListFrame.height
|
||||||
|
inputPanelBottomInset = viewListInset
|
||||||
|
} else if let viewList = self.viewList {
|
||||||
|
self.viewList = nil
|
||||||
|
if let viewListView = viewList.view.view {
|
||||||
|
transition.setPosition(view: viewListView, position: CGPoint(x: viewListView.center.x, y: availableSize.height + viewListView.bounds.height * 0.5), completion: { [weak viewListView] _ in
|
||||||
|
viewListView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentDefaultBottomInset: CGFloat = bottomContentInset
|
||||||
|
let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset)
|
||||||
|
|
||||||
|
let contentVisualBottomInset: CGFloat
|
||||||
|
if self.displayViewList {
|
||||||
|
contentVisualBottomInset = viewListInset + 12.0
|
||||||
|
} else {
|
||||||
|
contentVisualBottomInset = contentDefaultBottomInset
|
||||||
|
}
|
||||||
|
let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset
|
||||||
|
let contentVisualScale = contentVisualHeight / contentSize.height
|
||||||
|
|
||||||
|
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize)
|
||||||
|
|
||||||
|
transition.setPosition(view: self.contentContainerView, position: contentFrame.center)
|
||||||
|
transition.setBounds(view: self.contentContainerView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||||
|
transition.setScale(view: self.contentContainerView, scale: contentVisualScale)
|
||||||
|
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0 * (1.0 / contentVisualScale))
|
||||||
|
|
||||||
if self.closeButtonIconView.image == nil {
|
if self.closeButtonIconView.image == nil {
|
||||||
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
|
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
|
||||||
@ -1078,7 +1175,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0))
|
let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0))
|
||||||
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
||||||
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
|
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
|
||||||
transition.setAlpha(view: self.closeButton, alpha: component.hideUI ? 0.0 : 1.0)
|
transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let focusedItem: StoryContentItem? = component.slice.item
|
let focusedItem: StoryContentItem? = component.slice.item
|
||||||
@ -1148,7 +1245,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1174,13 +1271,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let gradientHeight: CGFloat = 74.0
|
let gradientHeight: CGFloat = 74.0
|
||||||
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
||||||
transition.setAlpha(layer: self.topContentGradientLayer, alpha: component.hideUI ? 0.0 : 1.0)
|
transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
|
|
||||||
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
|
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
|
||||||
self.itemLayout = itemLayout
|
self.itemLayout = itemLayout
|
||||||
@ -1230,7 +1327,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.addSubview(captionItemView)
|
self.addSubview(captionItemView)
|
||||||
}
|
}
|
||||||
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame)
|
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame)
|
||||||
captionItemTransition.setAlpha(view: captionItemView, alpha: component.hideUI ? 0.0 : 1.0)
|
captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1445,13 +1542,16 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
|
var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
|
||||||
|
if self.displayViewList {
|
||||||
|
footerPanelFrame.origin.y += footerPanelSize.height
|
||||||
|
}
|
||||||
if let footerPanelView = self.footerPanel.view {
|
if let footerPanelView = self.footerPanel.view {
|
||||||
if footerPanelView.superview == nil {
|
if footerPanelView.superview == nil {
|
||||||
self.addSubview(footerPanelView)
|
self.addSubview(footerPanelView)
|
||||||
}
|
}
|
||||||
transition.setFrame(view: footerPanelView, frame: footerPanelFrame)
|
transition.setFrame(view: footerPanelView, frame: footerPanelFrame)
|
||||||
transition.setAlpha(view: footerPanelView, alpha: focusedItem?.isMy == true ? 1.0 : 0.0)
|
transition.setAlpha(view: footerPanelView, alpha: (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let bottomGradientHeight = inputPanelSize.height + 32.0
|
let bottomGradientHeight = inputPanelSize.height + 32.0
|
||||||
@ -1464,7 +1564,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
normalDimAlpha = captionItem.externalState.expandFraction
|
normalDimAlpha = captionItem.externalState.expandFraction
|
||||||
}
|
}
|
||||||
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
|
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
|
||||||
if component.hideUI {
|
if component.hideUI || self.displayViewList {
|
||||||
dimAlpha = 0.0
|
dimAlpha = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1473,9 +1573,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
self.ignoreScrolling = true
|
self.ignoreScrolling = true
|
||||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||||
let contentSize = availableSize
|
let scrollContentSize = availableSize
|
||||||
if contentSize != self.scrollView.contentSize {
|
if scrollContentSize != self.scrollView.contentSize {
|
||||||
self.scrollView.contentSize = contentSize
|
self.scrollView.contentSize = scrollContentSize
|
||||||
}
|
}
|
||||||
self.ignoreScrolling = false
|
self.ignoreScrolling = false
|
||||||
self.updateScrolling(transition: transition)
|
self.updateScrolling(transition: transition)
|
||||||
@ -1505,7 +1605,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.contentContainerView.addSubview(navigationStripView)
|
self.contentContainerView.addSubview(navigationStripView)
|
||||||
}
|
}
|
||||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||||
transition.setAlpha(view: navigationStripView, alpha: component.hideUI ? 0.0 : 1.0)
|
transition.setAlpha(view: navigationStripView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var items: [StoryActionsComponent.Item] = []
|
var items: [StoryActionsComponent.Item] = []
|
||||||
@ -1542,7 +1642,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if self.displayReactions {
|
if self.displayReactions {
|
||||||
inlineActionsAlpha = 0.0
|
inlineActionsAlpha = 0.0
|
||||||
}
|
}
|
||||||
if component.hideUI {
|
if component.hideUI || self.displayViewList {
|
||||||
inlineActionsAlpha = 0.0
|
inlineActionsAlpha = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,510 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import AccountContext
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramStringFormatting
|
||||||
|
import ShimmerEffect
|
||||||
|
|
||||||
|
final class StoryItemSetViewListComponent: Component {
|
||||||
|
final class ExternalState {
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let externalState: ExternalState
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let safeInsets: UIEdgeInsets
|
||||||
|
let storyItem: EngineStoryItem
|
||||||
|
let close: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
externalState: ExternalState,
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
safeInsets: UIEdgeInsets,
|
||||||
|
storyItem: EngineStoryItem,
|
||||||
|
close: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.externalState = externalState
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.safeInsets = safeInsets
|
||||||
|
self.storyItem = storyItem
|
||||||
|
self.close = close
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.safeInsets != rhs.safeInsets {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.storyItem != rhs.storyItem {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ItemLayout: Equatable {
|
||||||
|
var containerSize: CGSize
|
||||||
|
var bottomInset: CGFloat
|
||||||
|
var topInset: CGFloat
|
||||||
|
var sideInset: CGFloat
|
||||||
|
var itemHeight: CGFloat
|
||||||
|
var itemCount: Int
|
||||||
|
|
||||||
|
var contentSize: CGSize
|
||||||
|
|
||||||
|
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
||||||
|
self.containerSize = containerSize
|
||||||
|
self.bottomInset = bottomInset
|
||||||
|
self.topInset = topInset
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.itemHeight = itemHeight
|
||||||
|
self.itemCount = itemCount
|
||||||
|
|
||||||
|
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||||
|
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||||
|
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
||||||
|
minVisibleRow = max(0, minVisibleRow)
|
||||||
|
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
||||||
|
|
||||||
|
let minVisibleIndex = minVisibleRow
|
||||||
|
let maxVisibleIndex = maxVisibleRow
|
||||||
|
|
||||||
|
if maxVisibleIndex >= minVisibleIndex {
|
||||||
|
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemFrame(for index: Int) -> CGRect {
|
||||||
|
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ScrollView: UIScrollView {
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView, UIScrollViewDelegate {
|
||||||
|
private let navigationBarBackground: BlurredBackgroundView
|
||||||
|
private let navigationSeparator: SimpleLayer
|
||||||
|
private let navigationTitle = ComponentView<Empty>()
|
||||||
|
private let navigationLeftButton = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let backgroundView: UIView
|
||||||
|
private let scrollView: UIScrollView
|
||||||
|
|
||||||
|
private var itemLayout: ItemLayout?
|
||||||
|
|
||||||
|
private let measureItem = ComponentView<Empty>()
|
||||||
|
private var placeholderImage: UIImage?
|
||||||
|
|
||||||
|
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||||
|
private var visiblePlaceholderViews: [Int: UIImageView] = [:]
|
||||||
|
|
||||||
|
private var component: StoryItemSetViewListComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
private var ignoreScrolling: Bool = false
|
||||||
|
|
||||||
|
private var viewList: EngineStoryViewListContext?
|
||||||
|
private var viewListDisposable: Disposable?
|
||||||
|
private var viewListState: EngineStoryViewListContext.State?
|
||||||
|
private var requestedLoadMoreToken: EngineStoryViewListContext.LoadMoreToken?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.navigationBarBackground = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
self.navigationSeparator = SimpleLayer()
|
||||||
|
|
||||||
|
self.backgroundView = UIView()
|
||||||
|
|
||||||
|
self.scrollView = ScrollView()
|
||||||
|
self.scrollView.canCancelContentTouches = true
|
||||||
|
self.scrollView.delaysContentTouches = false
|
||||||
|
self.scrollView.showsVerticalScrollIndicator = true
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
self.scrollView.alwaysBounceVertical = true
|
||||||
|
self.scrollView.indicatorStyle = .white
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
self.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.addSubview(self.navigationBarBackground)
|
||||||
|
self.layer.addSublayer(self.navigationSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.viewListDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if !self.ignoreScrolling {
|
||||||
|
self.updateScrolling(transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScrolling(transition: Transition) {
|
||||||
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||||
|
|
||||||
|
var synchronousLoad = false
|
||||||
|
if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
|
||||||
|
synchronousLoad = hint.synchronousLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
var validIds: [EnginePeer.Id] = []
|
||||||
|
var validPlaceholderIds: [Int] = []
|
||||||
|
if let range = itemLayout.visibleItems(for: visibleBounds) {
|
||||||
|
for index in range.lowerBound ..< range.upperBound {
|
||||||
|
guard let viewListState = self.viewListState, index < viewListState.totalCount else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemFrame = itemLayout.itemFrame(for: index)
|
||||||
|
|
||||||
|
if index >= viewListState.items.count {
|
||||||
|
validPlaceholderIds.append(index)
|
||||||
|
|
||||||
|
let placeholderView: UIImageView
|
||||||
|
if let current = self.visiblePlaceholderViews[index] {
|
||||||
|
placeholderView = current
|
||||||
|
} else {
|
||||||
|
placeholderView = UIImageView()
|
||||||
|
self.visiblePlaceholderViews[index] = placeholderView
|
||||||
|
self.scrollView.addSubview(placeholderView)
|
||||||
|
|
||||||
|
placeholderView.image = self.placeholderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholderView.frame = itemFrame
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemTransition = transition
|
||||||
|
let item = viewListState.items[index]
|
||||||
|
validIds.append(item.peer.id)
|
||||||
|
|
||||||
|
let visibleItem: ComponentView<Empty>
|
||||||
|
if let current = self.visibleItems[item.peer.id] {
|
||||||
|
visibleItem = current
|
||||||
|
} else {
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
itemTransition = .immediate
|
||||||
|
}
|
||||||
|
visibleItem = ComponentView()
|
||||||
|
self.visibleItems[item.peer.id] = visibleItem
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: item.timestamp, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
|
||||||
|
dateFormatString: { value in
|
||||||
|
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: [])
|
||||||
|
},
|
||||||
|
tomorrowFormatString: { value in
|
||||||
|
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
|
||||||
|
},
|
||||||
|
todayFormatString: { value in
|
||||||
|
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
|
||||||
|
},
|
||||||
|
yesterdayFormatString: { value in
|
||||||
|
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_YesterdayAt(value).string, ranges: [])
|
||||||
|
}
|
||||||
|
)).string
|
||||||
|
|
||||||
|
let _ = visibleItem.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
sideInset: itemLayout.sideInset,
|
||||||
|
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||||
|
peer: item.peer,
|
||||||
|
subtitle: dateText,
|
||||||
|
selectionState: .none,
|
||||||
|
hasNext: index != viewListState.totalCount - 1,
|
||||||
|
action: { _ in
|
||||||
|
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: itemFrame.size
|
||||||
|
)
|
||||||
|
if let itemView = visibleItem.view {
|
||||||
|
var animateIn = false
|
||||||
|
if itemView.superview == nil {
|
||||||
|
animateIn = true
|
||||||
|
self.scrollView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
|
||||||
|
if animateIn, synchronousLoad {
|
||||||
|
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeIds: [EnginePeer.Id] = []
|
||||||
|
for (id, visibleItem) in self.visibleItems {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
|
if let itemView = visibleItem.view {
|
||||||
|
itemView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
self.visibleItems.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var removePlaceholderIds: [Int] = []
|
||||||
|
for (id, placeholderView) in self.visiblePlaceholderViews {
|
||||||
|
if !validPlaceholderIds.contains(id) {
|
||||||
|
removePlaceholderIds.append(id)
|
||||||
|
|
||||||
|
if synchronousLoad {
|
||||||
|
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderView] _ in
|
||||||
|
placeholderView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
placeholderView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removePlaceholderIds {
|
||||||
|
self.visiblePlaceholderViews.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let viewList = self.viewList, let viewListState = self.viewListState, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
|
||||||
|
if self.requestedLoadMoreToken != viewListState.loadMoreToken {
|
||||||
|
self.requestedLoadMoreToken = viewListState.loadMoreToken
|
||||||
|
viewList.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
let itemUpdated = self.component?.storyItem.id != component.storyItem.id
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let size = CGSize(width: availableSize.width, height: min(availableSize.height, 500.0))
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
|
||||||
|
self.navigationBarBackground.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||||
|
self.navigationSeparator.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemUpdated {
|
||||||
|
self.viewListState = nil
|
||||||
|
self.viewList = nil
|
||||||
|
self.viewListDisposable?.dispose()
|
||||||
|
|
||||||
|
if let views = component.storyItem.views {
|
||||||
|
let viewList = component.context.engine.messages.storyViewList(id: component.storyItem.id, views: views)
|
||||||
|
self.viewList = viewList
|
||||||
|
var applyState = false
|
||||||
|
self.viewListDisposable = (viewList.state
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] listState in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.viewListState = listState
|
||||||
|
if applyState {
|
||||||
|
self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: true)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
applyState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
let navigationHeight: CGFloat = 56.0
|
||||||
|
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: navigationHeight))
|
||||||
|
transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame)
|
||||||
|
self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Button(
|
||||||
|
content: AnyComponent(Text(text: component.strings.Common_Close, font: Font.regular(17.0), color: component.theme.rootController.navigationBar.accentTextColor)),
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.close()
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 56.0))),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: navigationLeftButtonSize)
|
||||||
|
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||||
|
if navigationLeftButtonView.superview == nil {
|
||||||
|
self.addSubview(navigationLeftButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleText: String
|
||||||
|
|
||||||
|
let viewCount = self.viewListState?.totalCount ?? component.storyItem.views?.seenCount
|
||||||
|
if let viewCount {
|
||||||
|
if viewCount == 1 {
|
||||||
|
titleText = "1 View"
|
||||||
|
} else {
|
||||||
|
titleText = "\(viewCount) Views"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleText = "No Views"
|
||||||
|
}
|
||||||
|
let navigationTitleSize = self.navigationTitle.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(Text(
|
||||||
|
text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||||
|
)
|
||||||
|
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - navigationTitleSize.width) * 0.5), y: floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
|
||||||
|
if let navigationTitleView = self.navigationTitle.view {
|
||||||
|
if navigationTitleView.superview == nil {
|
||||||
|
self.addSubview(navigationTitleView)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center)
|
||||||
|
transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: size.height - navigationBarFrame.maxY)))
|
||||||
|
|
||||||
|
let measureItemSize = self.measureItem.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
sideInset: sideInset,
|
||||||
|
title: "AAAAAAAAAAAA",
|
||||||
|
peer: nil,
|
||||||
|
subtitle: "BBBBBBB",
|
||||||
|
selectionState: .none,
|
||||||
|
hasNext: true,
|
||||||
|
action: { _ in
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: size.width, height: 1000.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.placeholderImage == nil || themeUpdated {
|
||||||
|
self.placeholderImage = generateImage(CGSize(width: 300.0, height: measureItemSize.height), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1).cgColor)
|
||||||
|
|
||||||
|
if let measureItemView = self.measureItem.view as? PeerListItemComponent.View {
|
||||||
|
context.fillEllipse(in: measureItemView.avatarFrame)
|
||||||
|
let lineWidth: CGFloat = 8.0
|
||||||
|
|
||||||
|
if let titleFrame = measureItemView.titleFrame {
|
||||||
|
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - lineWidth * 0.5)), size: CGSize(width: titleFrame.width, height: lineWidth)), cornerRadius: lineWidth * 0.5).cgPath)
|
||||||
|
context.fillPath()
|
||||||
|
}
|
||||||
|
if let labelFrame = measureItemView.labelFrame {
|
||||||
|
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: labelFrame.minX, y: floor(labelFrame.midY - lineWidth * 0.5)), size: CGSize(width: labelFrame.width, height: lineWidth)), cornerRadius: lineWidth * 0.5).cgPath)
|
||||||
|
context.fillPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?.stretchableImage(withLeftCapWidth: 299, topCapHeight: 0)
|
||||||
|
for (_, placeholderView) in self.visiblePlaceholderViews {
|
||||||
|
placeholderView.image = self.placeholderImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemLayout = ItemLayout(
|
||||||
|
containerSize: size,
|
||||||
|
bottomInset: component.safeInsets.bottom,
|
||||||
|
topInset: 0.0,
|
||||||
|
sideInset: sideInset,
|
||||||
|
itemHeight: measureItemSize.height,
|
||||||
|
itemCount: self.viewListState?.items.count ?? 0
|
||||||
|
)
|
||||||
|
self.itemLayout = itemLayout
|
||||||
|
|
||||||
|
let scrollContentSize = itemLayout.contentSize
|
||||||
|
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
|
||||||
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||||
|
let scrollContentInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||||
|
let scrollIndicatorInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: component.safeInsets.bottom, right: 0.0)
|
||||||
|
if self.scrollView.contentInset != scrollContentInsets {
|
||||||
|
self.scrollView.contentInset = scrollContentInsets
|
||||||
|
}
|
||||||
|
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
|
||||||
|
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
|
||||||
|
}
|
||||||
|
if self.scrollView.contentSize != scrollContentSize {
|
||||||
|
self.scrollView.contentSize = scrollContentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
self.updateScrolling(transition: transition)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
private let peerId: EnginePeer.Id
|
private let peerId: EnginePeer.Id
|
||||||
|
|
||||||
private(set) var sliceValue: StoryContentContextState.FocusedSlice?
|
private(set) var sliceValue: StoryContentContextState.FocusedSlice?
|
||||||
|
fileprivate var nextItems: [EngineStoryItem] = []
|
||||||
|
|
||||||
let updated = Promise<Void>()
|
let updated = Promise<Void>()
|
||||||
|
|
||||||
@ -154,6 +155,25 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
isPublic: item.isPublic
|
isPublic: item.isPublic
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var nextItems: [EngineStoryItem] = []
|
||||||
|
for i in (focusedIndex + 1) ..< min(focusedIndex + 4, itemsView.items.count) {
|
||||||
|
if let item = itemsView.items[i].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||||
|
nextItems.append(EngineStoryItem(
|
||||||
|
id: item.id,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
media: EngineMedia(media),
|
||||||
|
text: item.text,
|
||||||
|
entities: item.entities,
|
||||||
|
views: nil,
|
||||||
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
|
isPinned: item.isPinned,
|
||||||
|
isExpired: item.isExpired,
|
||||||
|
isPublic: item.isPublic
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nextItems = nextItems
|
||||||
self.sliceValue = StoryContentContextState.FocusedSlice(
|
self.sliceValue = StoryContentContextState.FocusedSlice(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
item: StoryContentItem(
|
item: StoryContentItem(
|
||||||
@ -314,6 +334,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
private var requestedStoryKeys = Set<StoryKey>()
|
private var requestedStoryKeys = Set<StoryKey>()
|
||||||
private var requestStoryDisposables = DisposableSet()
|
private var requestStoryDisposables = DisposableSet()
|
||||||
|
|
||||||
|
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
focusedPeerId: EnginePeer.Id?
|
focusedPeerId: EnginePeer.Id?
|
||||||
@ -336,6 +358,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
deinit {
|
deinit {
|
||||||
self.storySubscriptionsDisposable?.dispose()
|
self.storySubscriptionsDisposable?.dispose()
|
||||||
self.requestStoryDisposables.dispose()
|
self.requestStoryDisposables.dispose()
|
||||||
|
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePeerContexts() {
|
private func updatePeerContexts() {
|
||||||
@ -486,6 +511,82 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
self.statePromise.set(.single(stateValue))
|
self.statePromise.set(.single(stateValue))
|
||||||
|
|
||||||
self.updatedPromise.set(.single(Void()))
|
self.updatedPromise.set(.single(Void()))
|
||||||
|
|
||||||
|
var possibleItems: [(EnginePeer, EngineStoryItem)] = []
|
||||||
|
if let slice = currentState.centralPeerContext.sliceValue {
|
||||||
|
for item in currentState.centralPeerContext.nextItems {
|
||||||
|
possibleItems.append((slice.peer, item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let nextPeerContext = currentState.nextPeerContext, let slice = nextPeerContext.sliceValue {
|
||||||
|
possibleItems.append((slice.peer, slice.item.storyItem))
|
||||||
|
for item in nextPeerContext.nextItems {
|
||||||
|
possibleItems.append((slice.peer, item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextPriority = 0
|
||||||
|
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
|
||||||
|
for i in 0 ..< min(possibleItems.count, 3) {
|
||||||
|
let peer = possibleItems[i].0
|
||||||
|
let item = possibleItems[i].1
|
||||||
|
if let peerReference = PeerReference(peer._asPeer()) {
|
||||||
|
if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource {
|
||||||
|
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource)
|
||||||
|
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||||
|
resource: resource,
|
||||||
|
size: nil,
|
||||||
|
priority: .top(position: nextPriority)
|
||||||
|
)
|
||||||
|
nextPriority += 1
|
||||||
|
} else if let file = item.media._asMedia() as? TelegramMediaFile {
|
||||||
|
if let preview = file.previewRepresentations.last {
|
||||||
|
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
|
||||||
|
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||||
|
resource: resource,
|
||||||
|
size: nil,
|
||||||
|
priority: .top(position: nextPriority)
|
||||||
|
)
|
||||||
|
nextPriority += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
|
||||||
|
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||||
|
resource: resource,
|
||||||
|
size: file.preloadSize,
|
||||||
|
priority: .top(position: nextPriority)
|
||||||
|
)
|
||||||
|
nextPriority += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var validIds: [MediaResourceId] = []
|
||||||
|
for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) {
|
||||||
|
let resource = info.resource
|
||||||
|
validIds.append(resource.resource.id)
|
||||||
|
if self.preloadStoryResourceDisposables[resource.resource.id] == nil {
|
||||||
|
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
|
||||||
|
if let size = info.size {
|
||||||
|
fetchRange = (0 ..< Int64(size), .default)
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
fetchRange = nil
|
||||||
|
#endif
|
||||||
|
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeIds: [MediaResourceId] = []
|
||||||
|
for (id, disposable) in self.preloadStoryResourceDisposables {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
self.preloadStoryResourceDisposables.removeValue(forKey: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resetSideStates() {
|
public func resetSideStates() {
|
||||||
|
@ -169,13 +169,19 @@ final class StoryItemContentComponent: Component {
|
|||||||
self.videoNode = videoNode
|
self.videoNode = videoNode
|
||||||
self.addSubnode(videoNode)
|
self.addSubnode(videoNode)
|
||||||
|
|
||||||
|
videoNode.playbackCompleted = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.environment?.presentationProgressUpdated(1.0)
|
||||||
|
}
|
||||||
videoNode.ownsContentNodeUpdated = { [weak self] value in
|
videoNode.ownsContentNodeUpdated = { [weak self] value in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if value {
|
if value {
|
||||||
self.videoNode?.seek(0.0)
|
self.videoNode?.seek(0.0)
|
||||||
self.videoNode?.playOnceWithSound(playAndRecord: false)
|
self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
videoNode.canAttachContent = true
|
videoNode.canAttachContent = true
|
||||||
@ -404,9 +410,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
wasSynchronous = false
|
wasSynchronous = false
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
self.performActionAfterImageContentLoaded(update: false)
|
self.performActionAfterImageContentLoaded(update: false)
|
||||||
#endif
|
|
||||||
|
|
||||||
self.fetchDisposable?.dispose()
|
self.fetchDisposable?.dispose()
|
||||||
self.fetchDisposable = nil
|
self.fetchDisposable = nil
|
||||||
|
@ -12,17 +12,20 @@ import TelegramCore
|
|||||||
public final class StoryFooterPanelComponent: Component {
|
public final class StoryFooterPanelComponent: Component {
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let storyItem: EngineStoryItem?
|
public let storyItem: EngineStoryItem?
|
||||||
|
public let expandViewStats: () -> Void
|
||||||
public let deleteAction: () -> Void
|
public let deleteAction: () -> Void
|
||||||
public let moreAction: (UIView, ContextGesture?) -> Void
|
public let moreAction: (UIView, ContextGesture?) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
storyItem: EngineStoryItem?,
|
storyItem: EngineStoryItem?,
|
||||||
|
expandViewStats: @escaping () -> Void,
|
||||||
deleteAction: @escaping () -> Void,
|
deleteAction: @escaping () -> Void,
|
||||||
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.storyItem = storyItem
|
self.storyItem = storyItem
|
||||||
|
self.expandViewStats = expandViewStats
|
||||||
self.deleteAction = deleteAction
|
self.deleteAction = deleteAction
|
||||||
self.moreAction = moreAction
|
self.moreAction = moreAction
|
||||||
}
|
}
|
||||||
@ -38,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class View: UIView {
|
public final class View: UIView {
|
||||||
|
private let viewStatsButton: HighlightableButton
|
||||||
private let viewStatsText = ComponentView<Empty>()
|
private let viewStatsText = ComponentView<Empty>()
|
||||||
private let deleteButton = ComponentView<Empty>()
|
private let deleteButton = ComponentView<Empty>()
|
||||||
private var moreButton: MoreHeaderButton?
|
private var moreButton: MoreHeaderButton?
|
||||||
@ -49,18 +53,31 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
self.viewStatsButton = HighlightableButton()
|
||||||
|
|
||||||
self.avatarsContext = AnimatedAvatarSetContext()
|
self.avatarsContext = AnimatedAvatarSetContext()
|
||||||
self.avatarsNode = AnimatedAvatarSetNode()
|
self.avatarsNode = AnimatedAvatarSetNode()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.avatarsNode.view)
|
self.avatarsNode.view.isUserInteractionEnabled = false
|
||||||
|
self.viewStatsButton.addSubview(self.avatarsNode.view)
|
||||||
|
self.addSubview(self.viewStatsButton)
|
||||||
|
|
||||||
|
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func viewStatsPressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.expandViewStats()
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -85,16 +102,22 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewsText: String
|
var viewCount = 0
|
||||||
if let views = component.storyItem?.views, views.seenCount != 0 {
|
if let views = component.storyItem?.views, views.seenCount != 0 {
|
||||||
if views.seenCount == 1 {
|
viewCount = views.seenCount
|
||||||
viewsText = "1 view"
|
|
||||||
} else {
|
|
||||||
viewsText = "\(views.seenCount) views"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewsText = "No views yet"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewsText: String
|
||||||
|
if viewCount == 0 {
|
||||||
|
viewsText = "No Views"
|
||||||
|
} else if viewCount == 1 {
|
||||||
|
viewsText = "1 view"
|
||||||
|
} else {
|
||||||
|
viewsText = "\(viewCount) views"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewStatsButton.isEnabled = viewCount != 0
|
||||||
|
|
||||||
let viewStatsTextSize = self.viewStatsText.update(
|
let viewStatsTextSize = self.viewStatsText.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
|
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
|
||||||
@ -105,12 +128,15 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
if let viewStatsTextView = self.viewStatsText.view {
|
if let viewStatsTextView = self.viewStatsText.view {
|
||||||
if viewStatsTextView.superview == nil {
|
if viewStatsTextView.superview == nil {
|
||||||
viewStatsTextView.layer.anchorPoint = CGPoint()
|
viewStatsTextView.layer.anchorPoint = CGPoint()
|
||||||
self.addSubview(viewStatsTextView)
|
viewStatsTextView.isUserInteractionEnabled = false
|
||||||
|
self.viewStatsButton.addSubview(viewStatsTextView)
|
||||||
}
|
}
|
||||||
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
|
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
|
||||||
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
|
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
|
||||||
|
|
||||||
var rightContentOffset: CGFloat = availableSize.width - 12.0
|
var rightContentOffset: CGFloat = availableSize.width - 12.0
|
||||||
|
|
||||||
let deleteButtonSize = self.deleteButton.update(
|
let deleteButtonSize = self.deleteButton.update(
|
||||||
|
@ -19,6 +19,7 @@ swift_library(
|
|||||||
"//submodules/SSignalKit/SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
"//submodules/TelegramPresentationData",
|
"//submodules/TelegramPresentationData",
|
||||||
"//submodules/AvatarNode",
|
"//submodules/AvatarNode",
|
||||||
|
"//submodules/ContextUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -24,6 +24,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
public let storySubscriptions: EngineStorySubscriptions?
|
public let storySubscriptions: EngineStorySubscriptions?
|
||||||
public let collapseFraction: CGFloat
|
public let collapseFraction: CGFloat
|
||||||
public let peerAction: (EnginePeer?) -> Void
|
public let peerAction: (EnginePeer?) -> Void
|
||||||
|
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
externalState: ExternalState,
|
externalState: ExternalState,
|
||||||
@ -32,7 +33,8 @@ public final class StoryPeerListComponent: Component {
|
|||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
storySubscriptions: EngineStorySubscriptions?,
|
storySubscriptions: EngineStorySubscriptions?,
|
||||||
collapseFraction: CGFloat,
|
collapseFraction: CGFloat,
|
||||||
peerAction: @escaping (EnginePeer?) -> Void
|
peerAction: @escaping (EnginePeer?) -> Void,
|
||||||
|
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -41,6 +43,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
self.storySubscriptions = storySubscriptions
|
self.storySubscriptions = storySubscriptions
|
||||||
self.collapseFraction = collapseFraction
|
self.collapseFraction = collapseFraction
|
||||||
self.peerAction = peerAction
|
self.peerAction = peerAction
|
||||||
|
self.contextPeerAction = contextPeerAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||||
@ -315,7 +318,8 @@ public final class StoryPeerListComponent: Component {
|
|||||||
collapsedWidth: collapsedItemWidth,
|
collapsedWidth: collapsedItemWidth,
|
||||||
leftNeighborDistance: leftNeighborDistance,
|
leftNeighborDistance: leftNeighborDistance,
|
||||||
rightNeighborDistance: rightNeighborDistance,
|
rightNeighborDistance: rightNeighborDistance,
|
||||||
action: component.peerAction
|
action: component.peerAction,
|
||||||
|
contextGesture: component.contextPeerAction
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: itemLayout.itemSize
|
containerSize: itemLayout.itemSize
|
||||||
|
@ -9,6 +9,8 @@ import TelegramCore
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AvatarNode
|
import AvatarNode
|
||||||
|
import ContextUI
|
||||||
|
import AsyncDisplayKit
|
||||||
|
|
||||||
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
|
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
|
||||||
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
|
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
|
||||||
@ -142,6 +144,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
public let leftNeighborDistance: CGFloat?
|
public let leftNeighborDistance: CGFloat?
|
||||||
public let rightNeighborDistance: CGFloat?
|
public let rightNeighborDistance: CGFloat?
|
||||||
public let action: (EnginePeer) -> Void
|
public let action: (EnginePeer) -> Void
|
||||||
|
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -155,7 +158,8 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
collapsedWidth: CGFloat,
|
collapsedWidth: CGFloat,
|
||||||
leftNeighborDistance: CGFloat?,
|
leftNeighborDistance: CGFloat?,
|
||||||
rightNeighborDistance: CGFloat?,
|
rightNeighborDistance: CGFloat?,
|
||||||
action: @escaping (EnginePeer) -> Void
|
action: @escaping (EnginePeer) -> Void,
|
||||||
|
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -169,6 +173,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.leftNeighborDistance = leftNeighborDistance
|
self.leftNeighborDistance = leftNeighborDistance
|
||||||
self.rightNeighborDistance = rightNeighborDistance
|
self.rightNeighborDistance = rightNeighborDistance
|
||||||
self.action = action
|
self.action = action
|
||||||
|
self.contextGesture = contextGesture
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
|
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
|
||||||
@ -208,7 +213,13 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class View: HighlightTrackingButton {
|
public final class View: UIView {
|
||||||
|
private let extractedContainerNode: ContextExtractedContentContainingNode
|
||||||
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
private let extractedBackgroundView: UIImageView
|
||||||
|
|
||||||
|
private let button: HighlightTrackingButton
|
||||||
|
|
||||||
private let avatarContainer: UIView
|
private let avatarContainer: UIView
|
||||||
private var avatarNode: AvatarNode?
|
private var avatarNode: AvatarNode?
|
||||||
private var avatarAddBadgeView: UIImageView?
|
private var avatarAddBadgeView: UIImageView?
|
||||||
@ -223,6 +234,13 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
private weak var componentState: EmptyComponentState?
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
|
self.button = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
self.extractedBackgroundView = UIImageView()
|
||||||
|
self.extractedBackgroundView.alpha = 0.0
|
||||||
|
|
||||||
self.avatarContainer = UIView()
|
self.avatarContainer = UIView()
|
||||||
self.avatarContainer.isUserInteractionEnabled = false
|
self.avatarContainer.isUserInteractionEnabled = false
|
||||||
|
|
||||||
@ -238,9 +256,16 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.avatarContainer)
|
self.extractedContainerNode.contentNode.view.addSubview(self.extractedBackgroundView)
|
||||||
|
|
||||||
self.layer.addSublayer(self.indicatorColorLayer)
|
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||||
|
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||||
|
self.addSubview(self.containerNode.view)
|
||||||
|
|
||||||
|
self.extractedContainerNode.contentNode.view.addSubview(self.button)
|
||||||
|
self.button.addSubview(self.avatarContainer)
|
||||||
|
|
||||||
|
self.button.layer.addSublayer(self.indicatorColorLayer)
|
||||||
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
|
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
|
||||||
self.indicatorColorLayer.mask = self.indicatorMaskLayer
|
self.indicatorColorLayer.mask = self.indicatorMaskLayer
|
||||||
|
|
||||||
@ -252,7 +277,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.indicatorShapeLayer.lineWidth = 2.0
|
self.indicatorShapeLayer.lineWidth = 2.0
|
||||||
self.indicatorShapeLayer.lineCap = .round
|
self.indicatorShapeLayer.lineCap = .round
|
||||||
|
|
||||||
self.highligthedChanged = { [weak self] highlighted in
|
self.button.highligthedChanged = { [weak self] highlighted in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -264,7 +289,36 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25)
|
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.button.isEnabled = false
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.button.isEnabled = true
|
||||||
|
}
|
||||||
|
component.contextGesture(self.extractedContainerNode, gesture, component.peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExtracted {
|
||||||
|
self.extractedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 24.0, color: component.theme.contextMenu.backgroundColor)
|
||||||
|
}
|
||||||
|
transition.updateAlpha(layer: self.extractedBackgroundView.layer, alpha: isExtracted ? 1.0 : 0.0, completion: { [weak self] _ in
|
||||||
|
if !isExtracted {
|
||||||
|
self?.extractedBackgroundView.image = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder: NSCoder) {
|
required public init?(coder: NSCoder) {
|
||||||
@ -283,10 +337,22 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let size = availableSize
|
||||||
|
|
||||||
|
transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
transition.setFrame(view: self.extractedBackgroundView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0))
|
||||||
|
|
||||||
|
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height))
|
||||||
|
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
|
||||||
let hadUnseen = self.component?.hasUnseen
|
let hadUnseen = self.component?.hasUnseen
|
||||||
let hadProgress = self.component?.progress != nil
|
let hadProgress = self.component?.progress != nil
|
||||||
let themeUpdated = self.component?.theme !== component.theme
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
self.componentState = state
|
self.componentState = state
|
||||||
|
|
||||||
@ -440,7 +506,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
titleView.layer.anchorPoint = CGPoint()
|
titleView.layer.anchorPoint = CGPoint()
|
||||||
titleView.isUserInteractionEnabled = false
|
titleView.isUserInteractionEnabled = false
|
||||||
self.addSubview(titleView)
|
self.button.addSubview(titleView)
|
||||||
}
|
}
|
||||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user