[WIP] Stories

This commit is contained in:
Ali 2023-06-03 00:53:24 +04:00
parent 0f345717f7
commit 7c38aaf1cb
25 changed files with 1697 additions and 117 deletions

View File

@ -1848,6 +1848,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let size = info.size { if let size = info.size {
fetchRange = (0 ..< Int64(size), .default) fetchRange = (0 ..< Int64(size), .default)
} }
#if DEBUG
fetchRange = nil
#endif
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start() self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
} }
} }
@ -2516,6 +2519,51 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(storyContainerScreen) self.push(storyContainerScreen)
}) })
} }
componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
guard let self else {
return
}
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self else {
return
}
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(self.navigationController as? NavigationController)?.pushViewController(controller)
})
})
})))
items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))
items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
}
} }
} }

View File

@ -914,34 +914,30 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if itemNode.listNode.isTracking { if itemNode.listNode.isTracking {
if case let .known(value) = offset { if case let .known(value) = offset {
if !self.storiesUnlocked { if !self.storiesUnlocked {
if value < -1.0 { if value < -50.0 {
self.storiesUnlocked = true self.storiesUnlocked = true
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { guard let self else {
return return
} }
HapticFeedback().impact()
self.currentItemNode.ignoreStoryInsetAdjustment = true self.currentItemNode.ignoreStoryInsetAdjustment = true
self.currentItemNode.allowInsetFixWhileTracking = true
self.onStoriesLockedUpdated?(true) self.onStoriesLockedUpdated?(true)
self.currentItemNode.ignoreStoryInsetAdjustment = false self.currentItemNode.ignoreStoryInsetAdjustment = false
self.currentItemNode.allowInsetFixWhileTracking = false
} }
} }
} }
} }
} else { } else if self.storiesUnlocked {
switch offset { switch offset {
case let .known(value): case let .known(value):
if value >= 94.0 { if value >= 94.0 {
if self.storiesUnlocked { self.storiesUnlocked = false
self.storiesUnlocked = false self.onStoriesLockedUpdated?(false)
self.currentItemNode.stopScrolling()
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.onStoriesLockedUpdated?(false)
}
}
} }
default: default:
break break
@ -957,7 +953,6 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if value > 94.0 { if value > 94.0 {
if self.storiesUnlocked { if self.storiesUnlocked {
self.storiesUnlocked = false self.storiesUnlocked = false
self.currentItemNode.stopScrolling()
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { guard let self else {
@ -1720,7 +1715,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let self else { guard let self else {
return return
} }
self.controller?.requestLayout(transition: .immediate) //self.controller?.requestLayout(transition: .immediate)
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
} }
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in

View File

@ -1214,6 +1214,8 @@ public final class ChatListNode: ListView {
super.init() super.init()
self.useMainQueueTransactions = true
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
self.verticalScrollIndicatorFollowsOverscroll = true self.verticalScrollIndicatorFollowsOverscroll = true
@ -3128,6 +3130,7 @@ public final class ChatListNode: ListView {
} }
var options = transition.options var options = transition.options
options.insert(.Synchronous)
if self.view.window != nil { if self.view.window != nil {
if !options.contains(.AnimateInsertion) { if !options.contains(.AnimateInsertion) {
options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousDrawing)

View File

@ -212,6 +212,62 @@ public struct Transition {
} }
} }
public func setFrameWithAdditivePosition(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
assert(view.layer.anchorPoint == CGPoint())
if view.frame == frame {
completion?(true)
return
}
var completedBounds: Bool?
var completedPosition: Bool?
let processCompletion: () -> Void = {
guard let completedBounds, let completedPosition else {
return
}
completion?(completedBounds && completedPosition)
}
self.setBounds(view: view, bounds: CGRect(origin: view.bounds.origin, size: frame.size), completion: { value in
completedBounds = value
processCompletion()
})
self.animatePosition(view: view, from: CGPoint(x: -frame.minX + view.layer.position.x, y: -frame.minY + view.layer.position.y), to: CGPoint(), additive: true, completion: { value in
completedPosition = value
processCompletion()
})
view.layer.position = frame.origin
}
public func setFrameWithAdditivePosition(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
assert(layer.anchorPoint == CGPoint())
if layer.frame == frame {
completion?(true)
return
}
var completedBounds: Bool?
var completedPosition: Bool?
let processCompletion: () -> Void = {
guard let completedBounds, let completedPosition else {
return
}
completion?(completedBounds && completedPosition)
}
self.setBounds(layer: layer, bounds: CGRect(origin: layer.bounds.origin, size: frame.size), completion: { value in
completedBounds = value
processCompletion()
})
self.animatePosition(layer: layer, from: CGPoint(x: -frame.minX + layer.position.x, y: -frame.minY + layer.position.y), to: CGPoint(), additive: true, completion: { value in
completedPosition = value
processCompletion()
})
layer.position = frame.origin
}
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) { public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
if view.bounds == bounds { if view.bounds == bounds {
completion?(true) completion?(true)

View File

@ -206,6 +206,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
public final var dynamicBounceEnabled = true public final var dynamicBounceEnabled = true
public final var rotated = false public final var rotated = false
public final var experimentalSnapScrollToItem = false public final var experimentalSnapScrollToItem = false
public final var useMainQueueTransactions = false
public final var scrollEnabled: Bool = true { public final var scrollEnabled: Bool = true {
didSet { didSet {
@ -250,6 +251,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
} }
public final var snapToBottomInsetUntilFirstInteraction: Bool = false public final var snapToBottomInsetUntilFirstInteraction: Bool = false
public final var allowInsetFixWhileTracking: Bool = false
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)? public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
@ -595,7 +597,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
action() action()
} }
}*/ }*/
DispatchQueue.main.async(execute: action) if self.useMainQueueTransactions && Thread.isMainThread {
action()
} else {
DispatchQueue.main.async(execute: action)
}
} }
private func beginReordering(itemNode: ListViewItemNode) { private func beginReordering(itemNode: ListViewItemNode) {
@ -980,7 +986,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.trackingOffset += -deltaY self.trackingOffset += -deltaY
} }
self.enqueueUpdateVisibleItems(synchronous: false) if self.useMainQueueTransactions {
DispatchQueue.main.async { [weak self] in
self?.enqueueUpdateVisibleItems(synchronous: false)
}
} else {
self.enqueueUpdateVisibleItems(synchronous: false)
}
var useScrollDynamics = false var useScrollDynamics = false
@ -1630,19 +1642,29 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
self.ignoreScrollingEvents = true self.ignoreScrollingEvents = true
if topItemFound && bottomItemFound { if topItemFound && bottomItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight) if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
}
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
self.scroller.contentOffset = self.lastContentOffset if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset
}
} else if topItemFound { } else if topItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
if self.scroller.contentOffset != self.lastContentOffset { if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset self.scroller.contentOffset = self.lastContentOffset
} }
} else if bottomItemFound { } else if bottomItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
self.scroller.contentOffset = self.lastContentOffset if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset
}
} else if self.itemNodes.isEmpty { } else if self.itemNodes.isEmpty {
self.scroller.contentSize = self.visibleSize self.scroller.contentSize = self.visibleSize
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero { if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
@ -1650,10 +1672,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.lastContentOffset = .zero self.lastContentOffset = .zero
} }
} else { } else {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 { if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
self.scroller.contentOffset = self.lastContentOffset if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset
}
} else { } else {
self.lastContentOffset = self.scroller.contentOffset self.lastContentOffset = self.scroller.contentOffset
} }
@ -1662,8 +1688,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
private func async(_ f: @escaping () -> Void) { private func async(_ f: @escaping () -> Void) {
DispatchQueue.global(qos: .userInteractive).async(execute: f) if self.useMainQueueTransactions {
//DispatchQueue.main.async(execute: f) if Thread.isMainThread {
f()
} else {
DispatchQueue.main.async(execute: f)
}
} else {
DispatchQueue.global(qos: .userInteractive).async(execute: f)
}
} }
private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -2951,7 +2984,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
var offsetFix: CGFloat var offsetFix: CGFloat
let insetDeltaOffsetFix: CGFloat = 0.0 let insetDeltaOffsetFix: CGFloat = 0.0
if self.isTracking || isExperimentalSnapToScrollToItem { if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem {
offsetFix = 0.0 offsetFix = 0.0
} else if self.snapToBottomInsetUntilFirstInteraction { } else if self.snapToBottomInsetUntilFirstInteraction {
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom

View File

@ -1450,7 +1450,7 @@ open class NavigationBar: ASDisplayNode {
if let titleView = titleView as? NavigationBarTitleView { if let titleView = titleView as? NavigationBarTitleView {
let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset) let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset)
let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), sideContentWidth: 0.0, transition: titleViewTransition) let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition)
} }
if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar { if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {

View File

@ -4,5 +4,5 @@ import UIKit
public protocol NavigationBarTitleView { public protocol NavigationBarTitleView {
func animateLayoutTransition() func animateLayoutTransition()
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect
} }

View File

@ -41,7 +41,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
} }
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
let leftInset: CGFloat = 0.0 let leftInset: CGFloat = 0.0
let rightInset: CGFloat = 0.0 let rightInset: CGFloat = 0.0
@ -56,7 +56,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
} }
return 0.0 return CGRect()
} }
func animateLayoutTransition() { func animateLayoutTransition() {

View File

@ -636,7 +636,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
if let (size, clearBounds) = self.validLayout { if let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
@ -644,11 +644,11 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
super.layoutSubviews() super.layoutSubviews()
if let (size, clearBounds) = self.validLayout { if let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds) self.validLayout = (size, clearBounds)
let titleSize = self.titleNode.updateLayout(size) let titleSize = self.titleNode.updateLayout(size)
@ -661,7 +661,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
self.titleNode.frame = titleFrame self.titleNode.frame = titleFrame
self.subtitleNode.frame = subtitleFrame self.subtitleNode.frame = subtitleFrame
return titleSize.width return titleFrame
} }
func animateLayoutTransition() { func animateLayoutTransition() {

View File

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

View File

@ -878,5 +878,9 @@ public extension TelegramEngine {
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> { public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit) return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
} }
public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext {
return EngineStoryViewListContext(account: self.account, storyId: id, views: views)
}
} }
} }

View File

@ -293,6 +293,7 @@ public final class ChatListHeaderComponent: Component {
var contentOffsetFraction: CGFloat = 0.0 var contentOffsetFraction: CGFloat = 0.0
private(set) var centerContentWidth: CGFloat = 0.0 private(set) var centerContentWidth: CGFloat = 0.0
private(set) var centerContentOffsetX: CGFloat = 0.0
init( init(
backPressed: @escaping () -> Void, backPressed: @escaping () -> Void,
@ -440,7 +441,7 @@ public final class ChatListHeaderComponent: Component {
} }
} }
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, size: CGSize, transition: Transition) { func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: Transition) {
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
@ -616,6 +617,8 @@ public final class ChatListHeaderComponent: Component {
} }
} }
var centerContentWidth: CGFloat = 0.0
var centerContentOffsetX: CGFloat = 0.0
if let chatListTitle = content.chatListTitle { if let chatListTitle = content.chatListTitle {
var chatListTitleTransition = transition var chatListTitleTransition = transition
let chatListTitleView: ChatListTitleView let chatListTitleView: ChatListTitleView
@ -633,8 +636,13 @@ public final class ChatListHeaderComponent: Component {
chatListTitleView.theme = theme chatListTitleView.theme = theme
chatListTitleView.strings = strings chatListTitleView.strings = strings
chatListTitleView.setTitle(chatListTitle, animated: false) chatListTitleView.setTitle(chatListTitle, animated: false)
let centerContentWidth = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), sideContentWidth: sideContentWidth, transition: transition.containedViewLayoutTransition) let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
self.centerContentWidth = centerContentWidth centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0)
//sideWidth + centerWidth + centerOffset = size.width
//let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5
let centerOffset = sideContentWidth
centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset)
chatListTitleView.openStatusSetup = { [weak self] sourceView in chatListTitleView.openStatusSetup = { [weak self] sourceView in
guard let self else { guard let self else {
@ -649,7 +657,14 @@ public final class ChatListHeaderComponent: Component {
self.toggleIsLocked() self.toggleIsLocked()
} }
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize)) let chatListTitleOffset: CGFloat
if chatListTitle.activity {
chatListTitleOffset = 0.0
} else {
chatListTitleOffset = (centerOffset + centerContentOffsetX) * sideContentFraction
}
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: chatListTitleOffset + floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
} else { } else {
if let chatListTitleView = self.chatListTitleView { if let chatListTitleView = self.chatListTitleView {
self.chatListTitleView = nil self.chatListTitleView = nil
@ -658,6 +673,8 @@ public final class ChatListHeaderComponent: Component {
} }
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil
self.centerContentWidth = centerContentWidth
self.centerContentOffsetX = centerContentOffsetX
} }
} }
@ -672,6 +689,7 @@ public final class ChatListHeaderComponent: Component {
private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private let storyPeerListExternalState = StoryPeerListComponent.ExternalState()
private var storyPeerList: ComponentView<Empty>? private var storyPeerList: ComponentView<Empty>?
public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyPeerAction: ((EnginePeer?) -> Void)?
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
private var effectiveContentView: ContentView? { private var effectiveContentView: ContentView? {
return self.secondaryContentView ?? self.primaryContentView return self.secondaryContentView ?? self.primaryContentView
@ -803,10 +821,16 @@ public final class ChatListHeaderComponent: Component {
return return
} }
self.storyPeerAction?(peer) self.storyPeerAction?(peer)
},
contextPeerAction: { [weak self] sourceNode, gesture, peer in
guard let self else {
return
}
self.storyContextPeerAction?(sourceNode, gesture, peer)
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: self.bounds.width, height: 94.0) containerSize: CGSize(width: availableSize.width, height: 94.0)
) )
} }
@ -851,7 +875,7 @@ public final class ChatListHeaderComponent: Component {
} }
} }
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth * (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
@ -890,7 +914,7 @@ public final class ChatListHeaderComponent: Component {
self.secondaryContentView = secondaryContentView self.secondaryContentView = secondaryContentView
self.addSubview(secondaryContentView) self.addSubview(secondaryContentView)
} }
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, size: availableSize, transition: secondaryContentTransition) secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition)
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
@ -946,7 +970,7 @@ public final class ChatListHeaderComponent: Component {
var defaultStoryListX: CGFloat = 0.0 var defaultStoryListX: CGFloat = 0.0
if let primaryContentView = self.primaryContentView { if let primaryContentView = self.primaryContentView {
defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5) defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5) + primaryContentView.centerContentOffsetX
} }
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 94.0))) storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 94.0)))

View File

@ -137,7 +137,9 @@ public final class ChatListNavigationBar: Component {
override public init(frame: CGRect) { override public init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
self.separatorLayer = SimpleLayer() self.separatorLayer = SimpleLayer()
self.separatorLayer.anchorPoint = CGPoint()
super.init(frame: frame) super.init(frame: frame)
@ -167,10 +169,7 @@ public final class ChatListNavigationBar: Component {
} }
public func applyScroll(offset: CGFloat, transition: Transition) { public func applyScroll(offset: CGFloat, transition: Transition) {
var transition = transition let transition = transition
if self.applyScrollFractionAnimator != nil {
transition = .immediate
}
self.rawScrollOffset = offset self.rawScrollOffset = offset
@ -211,9 +210,13 @@ public final class ChatListNavigationBar: Component {
let previousHeight = self.backgroundView.bounds.height let previousHeight = self.backgroundView.bounds.height
self.backgroundView.update(size: visibleSize, transition: transition.containedViewLayoutTransition) self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: visibleSize))
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel))) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0)))
transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true)
self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)
transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel)))
let searchContentNode: NavigationBarSearchContentNode let searchContentNode: NavigationBarSearchContentNode
if let current = self.searchContentNode { if let current = self.searchContentNode {
@ -247,6 +250,7 @@ public final class ChatListNavigationBar: Component {
component.activateSearch(searchContentNode) component.activateSearch(searchContentNode)
} }
) )
searchContentNode.view.layer.anchorPoint = CGPoint()
self.searchContentNode = searchContentNode self.searchContentNode = searchContentNode
self.addSubview(searchContentNode.view) self.addSubview(searchContentNode.view)
} }
@ -273,11 +277,17 @@ public final class ChatListNavigationBar: Component {
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
searchContentNode.expansionProgress = 1.0 - searchOffsetFraction searchContentNode.expansionProgress = 1.0 - searchOffsetFraction
transition.setFrame(view: searchContentNode.view, frame: searchFrame) transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame)
searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition)
var headerTransition = transition
if self.applyScrollFractionAnimator != nil {
headerTransition = .immediate
}
let headerContentSize = self.headerContent.update( let headerContentSize = self.headerContent.update(
transition: transition, transition: headerTransition,
component: AnyComponent(ChatListHeaderComponent( component: AnyComponent(ChatListHeaderComponent(
sideInset: component.sideInset + 16.0, sideInset: component.sideInset + 16.0,
primaryContent: component.primaryContent, primaryContent: component.primaryContent,
@ -318,9 +328,10 @@ public final class ChatListNavigationBar: Component {
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize) let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
if let headerContentView = self.headerContent.view { if let headerContentView = self.headerContent.view {
if headerContentView.superview == nil { if headerContentView.superview == nil {
headerContentView.layer.anchorPoint = CGPoint()
self.addSubview(headerContentView) self.addSubview(headerContentView)
} }
transition.setFrame(view: headerContentView, frame: headerContentFrame) transition.setFrameWithAdditivePosition(view: headerContentView, frame: headerContentFrame)
} }
if component.tabsNode !== self.tabsNode { if component.tabsNode !== self.tabsNode {
@ -342,7 +353,8 @@ public final class ChatListNavigationBar: Component {
let tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 46.0), size: CGSize(width: visibleSize.width, height: 46.0)) let tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 46.0), size: CGSize(width: visibleSize.width, height: 46.0))
if let disappearingTabsView = self.disappearingTabsView { if let disappearingTabsView = self.disappearingTabsView {
transition.setFrame(view: disappearingTabsView, frame: tabsFrame) disappearingTabsView.layer.anchorPoint = CGPoint()
transition.setFrameWithAdditivePosition(view: disappearingTabsView, frame: tabsFrame)
} }
if let tabsNode = component.tabsNode { if let tabsNode = component.tabsNode {
@ -350,6 +362,7 @@ public final class ChatListNavigationBar: Component {
var tabsNodeTransition = transition var tabsNodeTransition = transition
if tabsNode.view.superview !== self { if tabsNode.view.superview !== self {
tabsNode.view.layer.anchorPoint = CGPoint()
tabsNodeTransition = .immediate tabsNodeTransition = .immediate
self.addSubview(tabsNode.view) self.addSubview(tabsNode.view)
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
@ -359,7 +372,7 @@ public final class ChatListNavigationBar: Component {
} }
} }
tabsNodeTransition.setFrame(view: tabsNode.view, frame: tabsFrame) tabsNodeTransition.setFrameWithAdditivePosition(view: tabsNode.view, frame: tabsFrame)
} }
} }

View File

@ -62,7 +62,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
public var openStatusSetup: ((UIView) -> Void)? public var openStatusSetup: ((UIView) -> Void)?
private var validLayout: (CGSize, CGRect, CGFloat)? private var validLayout: (CGSize, CGRect)?
public var manualLayout: Bool = false public var manualLayout: Bool = false
@ -316,13 +316,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
override public func layoutSubviews() { override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds, sideContentWidth) self.validLayout = (size, clearBounds)
var indicatorPadding: CGFloat = 0.0 var indicatorPadding: CGFloat = 0.0
let indicatorSize = self.activityIndicator.bounds.size let indicatorSize = self.activityIndicator.bounds.size
@ -344,9 +344,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
let combinedHeight = titleSize.height let combinedHeight = titleSize.height
let combinedWidth = sideContentWidth + titleSize.width let combinedWidth = titleSize.width
var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0) + sideContentWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width) titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width)
@ -429,7 +429,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
} }
} }
return combinedWidth var resultFrame = titleFrame
if !self.lockView.isHidden {
resultFrame = resultFrame.union(lockFrame)
}
if let titleCredibilityIconView = self.titleCredibilityIconView {
resultFrame = resultFrame.union(titleCredibilityIconView.frame)
}
return resultFrame
} }
@objc private func buttonPressed() { @objc private func buttonPressed() {

View File

@ -118,7 +118,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
private let button: HighlightTrackingButtonNode private let button: HighlightTrackingButtonNode
var manualLayout: Bool = false var manualLayout: Bool = false
private var validLayout: (CGSize, CGRect, CGFloat)? private var validLayout: (CGSize, CGRect)?
private var titleLeftIcon: ChatTitleIcon = .none private var titleLeftIcon: ChatTitleIcon = .none
private var titleRightIcon: ChatTitleIcon = .none private var titleRightIcon: ChatTitleIcon = .none
@ -355,8 +355,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.button.isUserInteractionEnabled = isEnabled self.button.isUserInteractionEnabled = isEnabled
if !self.updateStatus() { if !self.updateStatus() {
if updated { if updated {
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.2, curve: .easeInOut)) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
} }
} }
} }
@ -605,8 +605,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
} }
if self.activityNode.transitionToState(state, animation: .slide) { if self.activityNode.transitionToState(state, animation: .slide) {
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.3, curve: .spring)) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
} }
return true return true
} else { } else {
@ -688,8 +688,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
override public func layoutSubviews() { override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
@ -704,14 +704,14 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.titleContent = titleContent self.titleContent = titleContent
let _ = self.updateStatus() let _ = self.updateStatus()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
} }
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds, sideContentWidth) self.validLayout = (size, clearBounds)
self.button.frame = clearBounds self.button.frame = clearBounds
self.contentContainer.frame = clearBounds self.contentContainer.frame = clearBounds
@ -851,7 +851,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0))) self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0)))
return titleFrame.width return titleFrame
} }
@objc private func buttonPressed() { @objc private func buttonPressed() {
@ -1015,7 +1015,7 @@ public final class ChatTitleComponent: Component {
} }
contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false) contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false)
let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), sideContentWidth: 0.0, transition: transition.containedViewLayoutTransition) let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize return availableSize

View File

@ -51,6 +51,8 @@ swift_library(
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramStringFormatting",
"//submodules/ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

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

View File

@ -159,6 +159,14 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
final class ViewList {
let externalState = StoryItemSetViewListComponent.ExternalState()
let view = ComponentView<Empty>()
init() {
}
}
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let sendMessageContext: StoryItemSetContainerSendMessage let sendMessageContext: StoryItemSetContainerSendMessage
@ -184,6 +192,9 @@ public final class StoryItemSetContainerComponent: Component {
let footerPanel = ComponentView<Empty>() let footerPanel = ComponentView<Empty>()
let inputPanelExternalState = MessageInputPanelComponent.ExternalState() let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
var displayViewList: Bool = false
var viewList: ViewList?
var itemLayout: ItemLayout? var itemLayout: ItemLayout?
var ignoreScrolling: Bool = false var ignoreScrolling: Bool = false
@ -388,6 +399,9 @@ public final class StoryItemSetContainerComponent: Component {
} else if self.displayReactions { } else if self.displayReactions {
self.displayReactions = false self.displayReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else if self.displayViewList {
self.displayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 { } else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 {
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View { if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
@ -485,7 +499,7 @@ public final class StoryItemSetContainerComponent: Component {
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size)) itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
if let view = view as? StoryContentItem.View { if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil) view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
} }
} }
@ -510,7 +524,7 @@ public final class StoryItemSetContainerComponent: Component {
for (_, visibleItem) in self.visibleItems { for (_, visibleItem) in self.visibleItems {
if let view = visibleItem.view.view { if let view = visibleItem.view.view {
if let view = view as? StoryContentItem.View { if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil) view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
} }
} }
} }
@ -869,6 +883,16 @@ public final class StoryItemSetContainerComponent: Component {
component: AnyComponent(StoryFooterPanelComponent( component: AnyComponent(StoryFooterPanelComponent(
context: component.context, context: component.context,
storyItem: currentItem?.storyItem, storyItem: currentItem?.storyItem,
expandViewStats: { [weak self] in
guard let self else {
return
}
if !self.displayViewList {
self.displayViewList = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
},
deleteAction: { [weak self] in deleteAction: { [weak self] in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
@ -1053,8 +1077,9 @@ public final class StoryItemSetContainerComponent: Component {
) )
let bottomContentInsetWithoutInput = bottomContentInset let bottomContentInsetWithoutInput = bottomContentInset
var viewListInset: CGFloat = 0.0
let inputPanelBottomInset: CGFloat var inputPanelBottomInset: CGFloat
let inputPanelIsOverlay: Bool let inputPanelIsOverlay: Bool
if component.inputHeight == 0.0 { if component.inputHeight == 0.0 {
inputPanelBottomInset = bottomContentInset inputPanelBottomInset = bottomContentInset
@ -1066,9 +1091,81 @@ public final class StoryItemSetContainerComponent: Component {
inputPanelIsOverlay = true inputPanelIsOverlay = true
} }
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - bottomContentInset)) if self.displayViewList {
transition.setFrame(view: self.contentContainerView, frame: contentFrame) let viewList: ViewList
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0) var viewListTransition = transition
if let current = self.viewList {
viewList = current
} else {
if !transition.animation.isImmediate {
viewListTransition = .immediate
}
viewList = ViewList()
self.viewList = viewList
}
let viewListSize = viewList.view.update(
transition: viewListTransition,
component: AnyComponent(StoryItemSetViewListComponent(
externalState: viewList.externalState,
context: component.context,
theme: component.theme,
strings: component.strings,
safeInsets: component.safeInsets,
storyItem: component.slice.item.storyItem,
close: { [weak self] in
guard let self else {
return
}
self.displayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
)),
environment: {},
containerSize: availableSize
)
let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize)
if let viewListView = viewList.view.view {
var animateIn = false
if viewListView.superview == nil {
self.addSubview(viewListView)
animateIn = true
}
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
if animateIn, !transition.animation.isImmediate {
transition.animatePosition(view: viewListView, from: CGPoint(x: 0.0, y: viewListFrame.height), to: CGPoint(), additive: true)
}
}
viewListInset = viewListFrame.height
inputPanelBottomInset = viewListInset
} else if let viewList = self.viewList {
self.viewList = nil
if let viewListView = viewList.view.view {
transition.setPosition(view: viewListView, position: CGPoint(x: viewListView.center.x, y: availableSize.height + viewListView.bounds.height * 0.5), completion: { [weak viewListView] _ in
viewListView?.removeFromSuperview()
})
}
}
let contentDefaultBottomInset: CGFloat = bottomContentInset
let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset)
let contentVisualBottomInset: CGFloat
if self.displayViewList {
contentVisualBottomInset = viewListInset + 12.0
} else {
contentVisualBottomInset = contentDefaultBottomInset
}
let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset
let contentVisualScale = contentVisualHeight / contentSize.height
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize)
transition.setPosition(view: self.contentContainerView, position: contentFrame.center)
transition.setBounds(view: self.contentContainerView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
transition.setScale(view: self.contentContainerView, scale: contentVisualScale)
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0 * (1.0 / contentVisualScale))
if self.closeButtonIconView.image == nil { if self.closeButtonIconView.image == nil {
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate) self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
@ -1078,7 +1175,7 @@ public final class StoryItemSetContainerComponent: Component {
let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0)) let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0))
transition.setFrame(view: self.closeButton, frame: closeButtonFrame) transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size)) transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
transition.setAlpha(view: self.closeButton, alpha: component.hideUI ? 0.0 : 1.0) transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
} }
let focusedItem: StoryContentItem? = component.slice.item let focusedItem: StoryContentItem? = component.slice.item
@ -1148,7 +1245,7 @@ public final class StoryItemSetContainerComponent: Component {
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} }
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0) transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
} }
} }
@ -1174,13 +1271,13 @@ public final class StoryItemSetContainerComponent: Component {
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) //view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0) transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
} }
} }
let gradientHeight: CGFloat = 74.0 let gradientHeight: CGFloat = 74.0
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight))) transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
transition.setAlpha(layer: self.topContentGradientLayer, alpha: component.hideUI ? 0.0 : 1.0) transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput)) let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
self.itemLayout = itemLayout self.itemLayout = itemLayout
@ -1230,7 +1327,7 @@ public final class StoryItemSetContainerComponent: Component {
self.addSubview(captionItemView) self.addSubview(captionItemView)
} }
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame) captionItemTransition.setFrame(view: captionItemView, frame: captionFrame)
captionItemTransition.setAlpha(view: captionItemView, alpha: component.hideUI ? 0.0 : 1.0) captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
} }
} }
@ -1445,13 +1542,16 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
let footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
if self.displayViewList {
footerPanelFrame.origin.y += footerPanelSize.height
}
if let footerPanelView = self.footerPanel.view { if let footerPanelView = self.footerPanel.view {
if footerPanelView.superview == nil { if footerPanelView.superview == nil {
self.addSubview(footerPanelView) self.addSubview(footerPanelView)
} }
transition.setFrame(view: footerPanelView, frame: footerPanelFrame) transition.setFrame(view: footerPanelView, frame: footerPanelFrame)
transition.setAlpha(view: footerPanelView, alpha: focusedItem?.isMy == true ? 1.0 : 0.0) transition.setAlpha(view: footerPanelView, alpha: (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0)
} }
let bottomGradientHeight = inputPanelSize.height + 32.0 let bottomGradientHeight = inputPanelSize.height + 32.0
@ -1464,7 +1564,7 @@ public final class StoryItemSetContainerComponent: Component {
normalDimAlpha = captionItem.externalState.expandFraction normalDimAlpha = captionItem.externalState.expandFraction
} }
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
if component.hideUI { if component.hideUI || self.displayViewList {
dimAlpha = 0.0 dimAlpha = 0.0
} }
@ -1473,9 +1573,9 @@ public final class StoryItemSetContainerComponent: Component {
self.ignoreScrolling = true self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = availableSize let scrollContentSize = availableSize
if contentSize != self.scrollView.contentSize { if scrollContentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize self.scrollView.contentSize = scrollContentSize
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
@ -1505,7 +1605,7 @@ public final class StoryItemSetContainerComponent: Component {
self.contentContainerView.addSubview(navigationStripView) self.contentContainerView.addSubview(navigationStripView)
} }
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0))) transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
transition.setAlpha(view: navigationStripView, alpha: component.hideUI ? 0.0 : 1.0) transition.setAlpha(view: navigationStripView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
} }
var items: [StoryActionsComponent.Item] = [] var items: [StoryActionsComponent.Item] = []
@ -1542,7 +1642,7 @@ public final class StoryItemSetContainerComponent: Component {
if self.displayReactions { if self.displayReactions {
inlineActionsAlpha = 0.0 inlineActionsAlpha = 0.0
} }
if component.hideUI { if component.hideUI || self.displayViewList {
inlineActionsAlpha = 0.0 inlineActionsAlpha = 0.0
} }

View File

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

View File

@ -19,6 +19,7 @@ public final class StoryContentContextImpl: StoryContentContext {
private let peerId: EnginePeer.Id private let peerId: EnginePeer.Id
private(set) var sliceValue: StoryContentContextState.FocusedSlice? private(set) var sliceValue: StoryContentContextState.FocusedSlice?
fileprivate var nextItems: [EngineStoryItem] = []
let updated = Promise<Void>() let updated = Promise<Void>()
@ -154,6 +155,25 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: item.isPublic isPublic: item.isPublic
) )
var nextItems: [EngineStoryItem] = []
for i in (focusedIndex + 1) ..< min(focusedIndex + 4, itemsView.items.count) {
if let item = itemsView.items[i].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
nextItems.append(EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
media: EngineMedia(media),
text: item.text,
entities: item.entities,
views: nil,
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic
))
}
}
self.nextItems = nextItems
self.sliceValue = StoryContentContextState.FocusedSlice( self.sliceValue = StoryContentContextState.FocusedSlice(
peer: peer, peer: peer,
item: StoryContentItem( item: StoryContentItem(
@ -314,6 +334,8 @@ public final class StoryContentContextImpl: StoryContentContext {
private var requestedStoryKeys = Set<StoryKey>() private var requestedStoryKeys = Set<StoryKey>()
private var requestStoryDisposables = DisposableSet() private var requestStoryDisposables = DisposableSet()
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
public init( public init(
context: AccountContext, context: AccountContext,
focusedPeerId: EnginePeer.Id? focusedPeerId: EnginePeer.Id?
@ -336,6 +358,9 @@ public final class StoryContentContextImpl: StoryContentContext {
deinit { deinit {
self.storySubscriptionsDisposable?.dispose() self.storySubscriptionsDisposable?.dispose()
self.requestStoryDisposables.dispose() self.requestStoryDisposables.dispose()
for (_, disposable) in self.preloadStoryResourceDisposables {
disposable.dispose()
}
} }
private func updatePeerContexts() { private func updatePeerContexts() {
@ -486,6 +511,82 @@ public final class StoryContentContextImpl: StoryContentContext {
self.statePromise.set(.single(stateValue)) self.statePromise.set(.single(stateValue))
self.updatedPromise.set(.single(Void())) self.updatedPromise.set(.single(Void()))
var possibleItems: [(EnginePeer, EngineStoryItem)] = []
if let slice = currentState.centralPeerContext.sliceValue {
for item in currentState.centralPeerContext.nextItems {
possibleItems.append((slice.peer, item))
}
}
if let nextPeerContext = currentState.nextPeerContext, let slice = nextPeerContext.sliceValue {
possibleItems.append((slice.peer, slice.item.storyItem))
for item in nextPeerContext.nextItems {
possibleItems.append((slice.peer, item))
}
}
var nextPriority = 0
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
for i in 0 ..< min(possibleItems.count, 3) {
let peer = possibleItems[i].0
let item = possibleItems[i].1
if let peerReference = PeerReference(peer._asPeer()) {
if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
} else if let file = item.media._asMedia() as? TelegramMediaFile {
if let preview = file.previewRepresentations.last {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: file.preloadSize,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
}
}
var validIds: [MediaResourceId] = []
for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) {
let resource = info.resource
validIds.append(resource.resource.id)
if self.preloadStoryResourceDisposables[resource.resource.id] == nil {
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
if let size = info.size {
fetchRange = (0 ..< Int64(size), .default)
}
#if DEBUG
fetchRange = nil
#endif
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
}
}
var removeIds: [MediaResourceId] = []
for (id, disposable) in self.preloadStoryResourceDisposables {
if !validIds.contains(id) {
removeIds.append(id)
disposable.dispose()
}
}
for id in removeIds {
self.preloadStoryResourceDisposables.removeValue(forKey: id)
}
} }
public func resetSideStates() { public func resetSideStates() {

View File

@ -169,13 +169,19 @@ final class StoryItemContentComponent: Component {
self.videoNode = videoNode self.videoNode = videoNode
self.addSubnode(videoNode) self.addSubnode(videoNode)
videoNode.playbackCompleted = { [weak self] in
guard let self else {
return
}
self.environment?.presentationProgressUpdated(1.0)
}
videoNode.ownsContentNodeUpdated = { [weak self] value in videoNode.ownsContentNodeUpdated = { [weak self] value in
guard let self else { guard let self else {
return return
} }
if value { if value {
self.videoNode?.seek(0.0) self.videoNode?.seek(0.0)
self.videoNode?.playOnceWithSound(playAndRecord: false) self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
} }
} }
videoNode.canAttachContent = true videoNode.canAttachContent = true
@ -404,9 +410,7 @@ final class StoryItemContentComponent: Component {
wasSynchronous = false wasSynchronous = false
} }
#if DEBUG
self.performActionAfterImageContentLoaded(update: false) self.performActionAfterImageContentLoaded(update: false)
#endif
self.fetchDisposable?.dispose() self.fetchDisposable?.dispose()
self.fetchDisposable = nil self.fetchDisposable = nil

View File

@ -12,17 +12,20 @@ import TelegramCore
public final class StoryFooterPanelComponent: Component { public final class StoryFooterPanelComponent: Component {
public let context: AccountContext public let context: AccountContext
public let storyItem: EngineStoryItem? public let storyItem: EngineStoryItem?
public let expandViewStats: () -> Void
public let deleteAction: () -> Void public let deleteAction: () -> Void
public let moreAction: (UIView, ContextGesture?) -> Void public let moreAction: (UIView, ContextGesture?) -> Void
public init( public init(
context: AccountContext, context: AccountContext,
storyItem: EngineStoryItem?, storyItem: EngineStoryItem?,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void moreAction: @escaping (UIView, ContextGesture?) -> Void
) { ) {
self.context = context self.context = context
self.storyItem = storyItem self.storyItem = storyItem
self.expandViewStats = expandViewStats
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.moreAction = moreAction self.moreAction = moreAction
} }
@ -38,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
private let viewStatsButton: HighlightableButton
private let viewStatsText = ComponentView<Empty>() private let viewStatsText = ComponentView<Empty>()
private let deleteButton = ComponentView<Empty>() private let deleteButton = ComponentView<Empty>()
private var moreButton: MoreHeaderButton? private var moreButton: MoreHeaderButton?
@ -49,18 +53,31 @@ public final class StoryFooterPanelComponent: Component {
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.viewStatsButton = HighlightableButton()
self.avatarsContext = AnimatedAvatarSetContext() self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode() self.avatarsNode = AnimatedAvatarSetNode()
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.avatarsNode.view) self.avatarsNode.view.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(self.avatarsNode.view)
self.addSubview(self.viewStatsButton)
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc private func viewStatsPressed() {
guard let component = self.component else {
return
}
component.expandViewStats()
}
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state self.state = state
@ -85,16 +102,22 @@ public final class StoryFooterPanelComponent: Component {
leftOffset = avatarsNodeFrame.maxX + avatarSpacing leftOffset = avatarsNodeFrame.maxX + avatarSpacing
} }
let viewsText: String var viewCount = 0
if let views = component.storyItem?.views, views.seenCount != 0 { if let views = component.storyItem?.views, views.seenCount != 0 {
if views.seenCount == 1 { viewCount = views.seenCount
viewsText = "1 view"
} else {
viewsText = "\(views.seenCount) views"
}
} else {
viewsText = "No views yet"
} }
let viewsText: String
if viewCount == 0 {
viewsText = "No Views"
} else if viewCount == 1 {
viewsText = "1 view"
} else {
viewsText = "\(viewCount) views"
}
self.viewStatsButton.isEnabled = viewCount != 0
let viewStatsTextSize = self.viewStatsText.update( let viewStatsTextSize = self.viewStatsText.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)), component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
@ -105,12 +128,15 @@ public final class StoryFooterPanelComponent: Component {
if let viewStatsTextView = self.viewStatsText.view { if let viewStatsTextView = self.viewStatsText.view {
if viewStatsTextView.superview == nil { if viewStatsTextView.superview == nil {
viewStatsTextView.layer.anchorPoint = CGPoint() viewStatsTextView.layer.anchorPoint = CGPoint()
self.addSubview(viewStatsTextView) viewStatsTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsTextView)
} }
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin) transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size)) transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
} }
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
var rightContentOffset: CGFloat = availableSize.width - 12.0 var rightContentOffset: CGFloat = availableSize.width - 12.0
let deleteButtonSize = self.deleteButton.update( let deleteButtonSize = self.deleteButton.update(

View File

@ -19,6 +19,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/ContextUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -24,6 +24,7 @@ public final class StoryPeerListComponent: Component {
public let storySubscriptions: EngineStorySubscriptions? public let storySubscriptions: EngineStorySubscriptions?
public let collapseFraction: CGFloat public let collapseFraction: CGFloat
public let peerAction: (EnginePeer?) -> Void public let peerAction: (EnginePeer?) -> Void
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -32,7 +33,8 @@ public final class StoryPeerListComponent: Component {
strings: PresentationStrings, strings: PresentationStrings,
storySubscriptions: EngineStorySubscriptions?, storySubscriptions: EngineStorySubscriptions?,
collapseFraction: CGFloat, collapseFraction: CGFloat,
peerAction: @escaping (EnginePeer?) -> Void peerAction: @escaping (EnginePeer?) -> Void,
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -41,6 +43,7 @@ public final class StoryPeerListComponent: Component {
self.storySubscriptions = storySubscriptions self.storySubscriptions = storySubscriptions
self.collapseFraction = collapseFraction self.collapseFraction = collapseFraction
self.peerAction = peerAction self.peerAction = peerAction
self.contextPeerAction = contextPeerAction
} }
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
@ -315,7 +318,8 @@ public final class StoryPeerListComponent: Component {
collapsedWidth: collapsedItemWidth, collapsedWidth: collapsedItemWidth,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
action: component.peerAction action: component.peerAction,
contextGesture: component.contextPeerAction
)), )),
environment: {}, environment: {},
containerSize: itemLayout.itemSize containerSize: itemLayout.itemSize

View File

@ -9,6 +9,8 @@ import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AvatarNode import AvatarNode
import ContextUI
import AsyncDisplayKit
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? { private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y) let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
@ -142,6 +144,7 @@ public final class StoryPeerListItemComponent: Component {
public let leftNeighborDistance: CGFloat? public let leftNeighborDistance: CGFloat?
public let rightNeighborDistance: CGFloat? public let rightNeighborDistance: CGFloat?
public let action: (EnginePeer) -> Void public let action: (EnginePeer) -> Void
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public init( public init(
context: AccountContext, context: AccountContext,
@ -155,7 +158,8 @@ public final class StoryPeerListItemComponent: Component {
collapsedWidth: CGFloat, collapsedWidth: CGFloat,
leftNeighborDistance: CGFloat?, leftNeighborDistance: CGFloat?,
rightNeighborDistance: CGFloat?, rightNeighborDistance: CGFloat?,
action: @escaping (EnginePeer) -> Void action: @escaping (EnginePeer) -> Void,
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -169,6 +173,7 @@ public final class StoryPeerListItemComponent: Component {
self.leftNeighborDistance = leftNeighborDistance self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance self.rightNeighborDistance = rightNeighborDistance
self.action = action self.action = action
self.contextGesture = contextGesture
} }
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool { public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
@ -208,7 +213,13 @@ public final class StoryPeerListItemComponent: Component {
return true return true
} }
public final class View: HighlightTrackingButton { public final class View: UIView {
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundView: UIImageView
private let button: HighlightTrackingButton
private let avatarContainer: UIView private let avatarContainer: UIView
private var avatarNode: AvatarNode? private var avatarNode: AvatarNode?
private var avatarAddBadgeView: UIImageView? private var avatarAddBadgeView: UIImageView?
@ -223,6 +234,13 @@ public final class StoryPeerListItemComponent: Component {
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.button = HighlightTrackingButton()
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundView = UIImageView()
self.extractedBackgroundView.alpha = 0.0
self.avatarContainer = UIView() self.avatarContainer = UIView()
self.avatarContainer.isUserInteractionEnabled = false self.avatarContainer.isUserInteractionEnabled = false
@ -238,9 +256,16 @@ public final class StoryPeerListItemComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.avatarContainer) self.extractedContainerNode.contentNode.view.addSubview(self.extractedBackgroundView)
self.layer.addSublayer(self.indicatorColorLayer) self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubview(self.containerNode.view)
self.extractedContainerNode.contentNode.view.addSubview(self.button)
self.button.addSubview(self.avatarContainer)
self.button.layer.addSublayer(self.indicatorColorLayer)
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer) self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
self.indicatorColorLayer.mask = self.indicatorMaskLayer self.indicatorColorLayer.mask = self.indicatorMaskLayer
@ -252,7 +277,7 @@ public final class StoryPeerListItemComponent: Component {
self.indicatorShapeLayer.lineWidth = 2.0 self.indicatorShapeLayer.lineWidth = 2.0
self.indicatorShapeLayer.lineCap = .round self.indicatorShapeLayer.lineCap = .round
self.highligthedChanged = { [weak self] highlighted in self.button.highligthedChanged = { [weak self] highlighted in
guard let self else { guard let self else {
return return
} }
@ -264,7 +289,36 @@ public final class StoryPeerListItemComponent: Component {
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25) self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25)
} }
} }
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerNode.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
return
}
self.button.isEnabled = false
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.button.isEnabled = true
}
component.contextGesture(self.extractedContainerNode, gesture, component.peer)
}
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let self, let component = self.component else {
return
}
if isExtracted {
self.extractedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 24.0, color: component.theme.contextMenu.backgroundColor)
}
transition.updateAlpha(layer: self.extractedBackgroundView.layer, alpha: isExtracted ? 1.0 : 0.0, completion: { [weak self] _ in
if !isExtracted {
self?.extractedBackgroundView.image = nil
}
})
}
} }
required public init?(coder: NSCoder) { required public init?(coder: NSCoder) {
@ -283,10 +337,22 @@ public final class StoryPeerListItemComponent: Component {
} }
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = availableSize
transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.extractedBackgroundView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0))
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
let hadUnseen = self.component?.hasUnseen let hadUnseen = self.component?.hasUnseen
let hadProgress = self.component?.progress != nil let hadProgress = self.component?.progress != nil
let themeUpdated = self.component?.theme !== component.theme let themeUpdated = self.component?.theme !== component.theme
self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId
self.component = component self.component = component
self.componentState = state self.componentState = state
@ -440,7 +506,7 @@ public final class StoryPeerListItemComponent: Component {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false titleView.isUserInteractionEnabled = false
self.addSubview(titleView) self.button.addSubview(titleView)
} }
transition.setPosition(view: titleView, position: titleFrame.origin) transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)