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

View File

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

View File

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

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) {
if view.bounds == bounds {
completion?(true)

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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