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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -2516,6 +2519,51 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
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 case let .known(value) = offset {
|
||||
if !self.storiesUnlocked {
|
||||
if value < -1.0 {
|
||||
if value < -50.0 {
|
||||
self.storiesUnlocked = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
HapticFeedback().impact()
|
||||
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = true
|
||||
self.currentItemNode.allowInsetFixWhileTracking = true
|
||||
self.onStoriesLockedUpdated?(true)
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = false
|
||||
self.currentItemNode.allowInsetFixWhileTracking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if self.storiesUnlocked {
|
||||
switch offset {
|
||||
case let .known(value):
|
||||
if value >= 94.0 {
|
||||
if self.storiesUnlocked {
|
||||
self.storiesUnlocked = false
|
||||
self.currentItemNode.stopScrolling()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.onStoriesLockedUpdated?(false)
|
||||
}
|
||||
}
|
||||
self.storiesUnlocked = false
|
||||
self.onStoriesLockedUpdated?(false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -957,7 +953,6 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
if value > 94.0 {
|
||||
if self.storiesUnlocked {
|
||||
self.storiesUnlocked = false
|
||||
self.currentItemNode.stopScrolling()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
@ -1720,7 +1715,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
guard let self else {
|
||||
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
|
||||
|
@ -1214,6 +1214,8 @@ public final class ChatListNode: ListView {
|
||||
|
||||
super.init()
|
||||
|
||||
self.useMainQueueTransactions = true
|
||||
|
||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||
|
||||
@ -3128,6 +3130,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
|
||||
var options = transition.options
|
||||
options.insert(.Synchronous)
|
||||
if self.view.window != nil {
|
||||
if !options.contains(.AnimateInsertion) {
|
||||
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) {
|
||||
if view.bounds == bounds {
|
||||
completion?(true)
|
||||
|
@ -206,6 +206,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
public final var dynamicBounceEnabled = true
|
||||
public final var rotated = false
|
||||
public final var experimentalSnapScrollToItem = false
|
||||
public final var useMainQueueTransactions = false
|
||||
|
||||
public final var scrollEnabled: Bool = true {
|
||||
didSet {
|
||||
@ -250,6 +251,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
}
|
||||
public final var snapToBottomInsetUntilFirstInteraction: Bool = false
|
||||
public final var allowInsetFixWhileTracking: Bool = false
|
||||
|
||||
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
||||
@ -595,7 +597,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
action()
|
||||
}
|
||||
}*/
|
||||
DispatchQueue.main.async(execute: action)
|
||||
if self.useMainQueueTransactions && Thread.isMainThread {
|
||||
action()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: action)
|
||||
}
|
||||
}
|
||||
|
||||
private func beginReordering(itemNode: ListViewItemNode) {
|
||||
@ -980,7 +986,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
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
|
||||
|
||||
@ -1630,19 +1642,29 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
|
||||
self.ignoreScrollingEvents = true
|
||||
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.scroller.contentOffset = self.lastContentOffset
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} 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)
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} 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.scroller.contentOffset = self.lastContentOffset
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} else if self.itemNodes.isEmpty {
|
||||
self.scroller.contentSize = self.visibleSize
|
||||
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
|
||||
@ -1650,10 +1672,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.lastContentOffset = .zero
|
||||
}
|
||||
} 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 {
|
||||
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 {
|
||||
self.lastContentOffset = self.scroller.contentOffset
|
||||
}
|
||||
@ -1662,8 +1688,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
|
||||
private func async(_ f: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .userInteractive).async(execute: f)
|
||||
//DispatchQueue.main.async(execute: f)
|
||||
if self.useMainQueueTransactions {
|
||||
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) {
|
||||
@ -2951,7 +2984,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
|
||||
var offsetFix: CGFloat
|
||||
let insetDeltaOffsetFix: CGFloat = 0.0
|
||||
if self.isTracking || isExperimentalSnapToScrollToItem {
|
||||
if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem {
|
||||
offsetFix = 0.0
|
||||
} else if self.snapToBottomInsetUntilFirstInteraction {
|
||||
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom
|
||||
|
@ -1450,7 +1450,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
if let titleView = titleView as? NavigationBarTitleView {
|
||||
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 {
|
||||
|
@ -4,5 +4,5 @@ import UIKit
|
||||
public protocol NavigationBarTitleView {
|
||||
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)
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
return 0.0
|
||||
return CGRect()
|
||||
}
|
||||
|
||||
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.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(size)
|
||||
@ -661,7 +661,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
||||
self.titleNode.frame = titleFrame
|
||||
self.subtitleNode.frame = subtitleFrame
|
||||
|
||||
return titleSize.width
|
||||
return titleFrame
|
||||
}
|
||||
|
||||
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> {
|
||||
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
|
||||
private(set) var centerContentWidth: CGFloat = 0.0
|
||||
private(set) var centerContentOffsetX: CGFloat = 0.0
|
||||
|
||||
init(
|
||||
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.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 {
|
||||
var chatListTitleTransition = transition
|
||||
let chatListTitleView: ChatListTitleView
|
||||
@ -633,8 +636,13 @@ public final class ChatListHeaderComponent: Component {
|
||||
chatListTitleView.theme = theme
|
||||
chatListTitleView.strings = strings
|
||||
chatListTitleView.setTitle(chatListTitle, animated: false)
|
||||
let centerContentWidth = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), sideContentWidth: sideContentWidth, transition: transition.containedViewLayoutTransition)
|
||||
self.centerContentWidth = centerContentWidth
|
||||
let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
|
||||
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
|
||||
guard let self else {
|
||||
@ -649,7 +657,14 @@ public final class ChatListHeaderComponent: Component {
|
||||
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 {
|
||||
if let chatListTitleView = self.chatListTitleView {
|
||||
self.chatListTitleView = nil
|
||||
@ -658,6 +673,8 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
|
||||
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 var storyPeerList: ComponentView<Empty>?
|
||||
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
||||
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
|
||||
|
||||
private var effectiveContentView: ContentView? {
|
||||
return self.secondaryContentView ?? self.primaryContentView
|
||||
@ -803,10 +821,16 @@ public final class ChatListHeaderComponent: Component {
|
||||
return
|
||||
}
|
||||
self.storyPeerAction?(peer)
|
||||
},
|
||||
contextPeerAction: { [weak self] sourceNode, gesture, peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.storyContextPeerAction?(sourceNode, gesture, peer)
|
||||
}
|
||||
)),
|
||||
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))
|
||||
|
||||
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
|
||||
@ -890,7 +914,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.secondaryContentView = 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))
|
||||
|
||||
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
|
||||
@ -946,7 +970,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
var defaultStoryListX: CGFloat = 0.0
|
||||
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)))
|
||||
|
@ -137,7 +137,9 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
self.separatorLayer = SimpleLayer()
|
||||
self.separatorLayer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -167,10 +169,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
|
||||
public func applyScroll(offset: CGFloat, transition: Transition) {
|
||||
var transition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
transition = .immediate
|
||||
}
|
||||
let transition = transition
|
||||
|
||||
self.rawScrollOffset = offset
|
||||
|
||||
@ -211,9 +210,13 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
let previousHeight = self.backgroundView.bounds.height
|
||||
|
||||
self.backgroundView.update(size: visibleSize, 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)))
|
||||
self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
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
|
||||
if let current = self.searchContentNode {
|
||||
@ -247,6 +250,7 @@ public final class ChatListNavigationBar: Component {
|
||||
component.activateSearch(searchContentNode)
|
||||
}
|
||||
)
|
||||
searchContentNode.view.layer.anchorPoint = CGPoint()
|
||||
self.searchContentNode = searchContentNode
|
||||
self.addSubview(searchContentNode.view)
|
||||
}
|
||||
@ -273,11 +277,17 @@ public final class ChatListNavigationBar: Component {
|
||||
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
|
||||
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)
|
||||
|
||||
var headerTransition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
headerTransition = .immediate
|
||||
}
|
||||
|
||||
let headerContentSize = self.headerContent.update(
|
||||
transition: transition,
|
||||
transition: headerTransition,
|
||||
component: AnyComponent(ChatListHeaderComponent(
|
||||
sideInset: component.sideInset + 16.0,
|
||||
primaryContent: component.primaryContent,
|
||||
@ -318,9 +328,10 @@ public final class ChatListNavigationBar: Component {
|
||||
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
||||
if let headerContentView = self.headerContent.view {
|
||||
if headerContentView.superview == nil {
|
||||
headerContentView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(headerContentView)
|
||||
}
|
||||
transition.setFrame(view: headerContentView, frame: headerContentFrame)
|
||||
transition.setFrameWithAdditivePosition(view: headerContentView, frame: headerContentFrame)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
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 {
|
||||
@ -350,6 +362,7 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
var tabsNodeTransition = transition
|
||||
if tabsNode.view.superview !== self {
|
||||
tabsNode.view.layer.anchorPoint = CGPoint()
|
||||
tabsNodeTransition = .immediate
|
||||
self.addSubview(tabsNode.view)
|
||||
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)?
|
||||
|
||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
public var manualLayout: Bool = false
|
||||
|
||||
@ -316,13 +316,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
var indicatorPadding: CGFloat = 0.0
|
||||
let indicatorSize = self.activityIndicator.bounds.size
|
||||
@ -344,9 +344,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
|
||||
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)
|
||||
|
||||
@ -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() {
|
||||
|
@ -118,7 +118,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
private let button: HighlightTrackingButtonNode
|
||||
|
||||
var manualLayout: Bool = false
|
||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
private var titleLeftIcon: ChatTitleIcon = .none
|
||||
private var titleRightIcon: ChatTitleIcon = .none
|
||||
@ -355,8 +355,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.button.isUserInteractionEnabled = isEnabled
|
||||
if !self.updateStatus() {
|
||||
if updated {
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
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.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.3, curve: .spring))
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
@ -688,8 +688,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,14 +704,14 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.titleContent = titleContent
|
||||
let _ = self.updateStatus()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
self.button.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)))
|
||||
|
||||
return titleFrame.width
|
||||
return titleFrame
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
@ -1015,7 +1015,7 @@ public final class ChatTitleComponent: Component {
|
||||
}
|
||||
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))
|
||||
|
||||
return availableSize
|
||||
|
@ -51,6 +51,8 @@ swift_library(
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//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 {
|
||||
let sendMessageContext: StoryItemSetContainerSendMessage
|
||||
|
||||
@ -184,6 +192,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let footerPanel = ComponentView<Empty>()
|
||||
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
|
||||
var displayViewList: Bool = false
|
||||
var viewList: ViewList?
|
||||
|
||||
var itemLayout: ItemLayout?
|
||||
var ignoreScrolling: Bool = false
|
||||
|
||||
@ -388,6 +399,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
} else if self.displayReactions {
|
||||
self.displayReactions = false
|
||||
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 {
|
||||
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
||||
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))
|
||||
|
||||
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 {
|
||||
if let view = visibleItem.view.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(
|
||||
context: component.context,
|
||||
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
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -1053,8 +1077,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
|
||||
let bottomContentInsetWithoutInput = bottomContentInset
|
||||
var viewListInset: CGFloat = 0.0
|
||||
|
||||
let inputPanelBottomInset: CGFloat
|
||||
var inputPanelBottomInset: CGFloat
|
||||
let inputPanelIsOverlay: Bool
|
||||
if component.inputHeight == 0.0 {
|
||||
inputPanelBottomInset = bottomContentInset
|
||||
@ -1066,9 +1091,81 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
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))
|
||||
transition.setFrame(view: self.contentContainerView, frame: contentFrame)
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0)
|
||||
if self.displayViewList {
|
||||
let viewList: ViewList
|
||||
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 {
|
||||
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))
|
||||
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.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
|
||||
@ -1148,7 +1245,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
self.itemLayout = itemLayout
|
||||
@ -1230,7 +1327,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.addSubview(captionItemView)
|
||||
}
|
||||
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 footerPanelView.superview == nil {
|
||||
self.addSubview(footerPanelView)
|
||||
}
|
||||
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
|
||||
@ -1464,7 +1564,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
normalDimAlpha = captionItem.externalState.expandFraction
|
||||
}
|
||||
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
|
||||
if component.hideUI {
|
||||
if component.hideUI || self.displayViewList {
|
||||
dimAlpha = 0.0
|
||||
}
|
||||
|
||||
@ -1473,9 +1573,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
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)))
|
||||
let contentSize = availableSize
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
let scrollContentSize = availableSize
|
||||
if scrollContentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = scrollContentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
@ -1505,7 +1605,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
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.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] = []
|
||||
@ -1542,7 +1642,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if self.displayReactions {
|
||||
inlineActionsAlpha = 0.0
|
||||
}
|
||||
if component.hideUI {
|
||||
if component.hideUI || self.displayViewList {
|
||||
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(set) var sliceValue: StoryContentContextState.FocusedSlice?
|
||||
fileprivate var nextItems: [EngineStoryItem] = []
|
||||
|
||||
let updated = Promise<Void>()
|
||||
|
||||
@ -154,6 +155,25 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
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(
|
||||
peer: peer,
|
||||
item: StoryContentItem(
|
||||
@ -314,6 +334,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
private var requestedStoryKeys = Set<StoryKey>()
|
||||
private var requestStoryDisposables = DisposableSet()
|
||||
|
||||
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
focusedPeerId: EnginePeer.Id?
|
||||
@ -336,6 +358,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
deinit {
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
self.requestStoryDisposables.dispose()
|
||||
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePeerContexts() {
|
||||
@ -486,6 +511,82 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
self.statePromise.set(.single(stateValue))
|
||||
|
||||
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() {
|
||||
|
@ -169,13 +169,19 @@ final class StoryItemContentComponent: Component {
|
||||
self.videoNode = 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
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
self.videoNode?.seek(0.0)
|
||||
self.videoNode?.playOnceWithSound(playAndRecord: false)
|
||||
self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
|
||||
}
|
||||
}
|
||||
videoNode.canAttachContent = true
|
||||
@ -404,9 +410,7 @@ final class StoryItemContentComponent: Component {
|
||||
wasSynchronous = false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
self.performActionAfterImageContentLoaded(update: false)
|
||||
#endif
|
||||
|
||||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
|
@ -12,17 +12,20 @@ import TelegramCore
|
||||
public final class StoryFooterPanelComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let storyItem: EngineStoryItem?
|
||||
public let expandViewStats: () -> Void
|
||||
public let deleteAction: () -> Void
|
||||
public let moreAction: (UIView, ContextGesture?) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
storyItem: EngineStoryItem?,
|
||||
expandViewStats: @escaping () -> Void,
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.storyItem = storyItem
|
||||
self.expandViewStats = expandViewStats
|
||||
self.deleteAction = deleteAction
|
||||
self.moreAction = moreAction
|
||||
}
|
||||
@ -38,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let viewStatsButton: HighlightableButton
|
||||
private let viewStatsText = ComponentView<Empty>()
|
||||
private let deleteButton = ComponentView<Empty>()
|
||||
private var moreButton: MoreHeaderButton?
|
||||
@ -49,18 +53,31 @@ public final class StoryFooterPanelComponent: Component {
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.viewStatsButton = HighlightableButton()
|
||||
|
||||
self.avatarsContext = AnimatedAvatarSetContext()
|
||||
self.avatarsNode = AnimatedAvatarSetNode()
|
||||
|
||||
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) {
|
||||
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 {
|
||||
self.component = component
|
||||
self.state = state
|
||||
@ -85,16 +102,22 @@ public final class StoryFooterPanelComponent: Component {
|
||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||
}
|
||||
|
||||
let viewsText: String
|
||||
var viewCount = 0
|
||||
if let views = component.storyItem?.views, views.seenCount != 0 {
|
||||
if views.seenCount == 1 {
|
||||
viewsText = "1 view"
|
||||
} else {
|
||||
viewsText = "\(views.seenCount) views"
|
||||
}
|
||||
} else {
|
||||
viewsText = "No views yet"
|
||||
viewCount = views.seenCount
|
||||
}
|
||||
|
||||
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(
|
||||
transition: .immediate,
|
||||
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 viewStatsTextView.superview == nil {
|
||||
viewStatsTextView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(viewStatsTextView)
|
||||
viewStatsTextView.isUserInteractionEnabled = false
|
||||
self.viewStatsButton.addSubview(viewStatsTextView)
|
||||
}
|
||||
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
|
||||
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
|
||||
|
||||
let deleteButtonSize = self.deleteButton.update(
|
||||
|
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -24,6 +24,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let collapseFraction: CGFloat
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -32,7 +33,8 @@ public final class StoryPeerListComponent: Component {
|
||||
strings: PresentationStrings,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
collapseFraction: CGFloat,
|
||||
peerAction: @escaping (EnginePeer?) -> Void
|
||||
peerAction: @escaping (EnginePeer?) -> Void,
|
||||
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -41,6 +43,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.collapseFraction = collapseFraction
|
||||
self.peerAction = peerAction
|
||||
self.contextPeerAction = contextPeerAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||
@ -315,7 +318,8 @@ public final class StoryPeerListComponent: Component {
|
||||
collapsedWidth: collapsedItemWidth,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
action: component.peerAction
|
||||
action: component.peerAction,
|
||||
contextGesture: component.contextPeerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
|
@ -9,6 +9,8 @@ import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import ContextUI
|
||||
import AsyncDisplayKit
|
||||
|
||||
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)
|
||||
@ -142,6 +144,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
public let leftNeighborDistance: CGFloat?
|
||||
public let rightNeighborDistance: CGFloat?
|
||||
public let action: (EnginePeer) -> Void
|
||||
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -155,7 +158,8 @@ public final class StoryPeerListItemComponent: Component {
|
||||
collapsedWidth: CGFloat,
|
||||
leftNeighborDistance: CGFloat?,
|
||||
rightNeighborDistance: CGFloat?,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -169,6 +173,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.leftNeighborDistance = leftNeighborDistance
|
||||
self.rightNeighborDistance = rightNeighborDistance
|
||||
self.action = action
|
||||
self.contextGesture = contextGesture
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
|
||||
@ -208,7 +213,13 @@ public final class StoryPeerListItemComponent: Component {
|
||||
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 var avatarNode: AvatarNode?
|
||||
private var avatarAddBadgeView: UIImageView?
|
||||
@ -223,6 +234,13 @@ public final class StoryPeerListItemComponent: Component {
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
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.isUserInteractionEnabled = false
|
||||
|
||||
@ -238,9 +256,16 @@ public final class StoryPeerListItemComponent: Component {
|
||||
|
||||
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.indicatorColorLayer.mask = self.indicatorMaskLayer
|
||||
|
||||
@ -252,7 +277,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.indicatorShapeLayer.lineWidth = 2.0
|
||||
self.indicatorShapeLayer.lineCap = .round
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -264,7 +289,36 @@ public final class StoryPeerListItemComponent: Component {
|
||||
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) {
|
||||
@ -283,10 +337,22 @@ public final class StoryPeerListItemComponent: Component {
|
||||
}
|
||||
|
||||
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 hadProgress = self.component?.progress != nil
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId
|
||||
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
@ -440,7 +506,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint()
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.button.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
|
Loading…
x
Reference in New Issue
Block a user