mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories
This commit is contained in:
parent
5a5adfb5b3
commit
82f511c8a5
@ -273,9 +273,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
private let isReorderingTabsValue = ValuePromise<Bool>(false)
|
||||
|
||||
private let navigationSecondaryContentNode: ASDisplayNode
|
||||
let tabsNode: SparseNode
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
|
||||
var hasTabs: Bool {
|
||||
if let tabContainerData = self.tabContainerData {
|
||||
let isEmpty = tabContainerData.0.count <= 1 || tabContainerData.1
|
||||
return !isEmpty
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var searchTabsNode: SparseNode?
|
||||
|
||||
private var hasDownloads: Bool = false
|
||||
private var activeDownloadsDisposable: Disposable?
|
||||
@ -336,9 +346,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
groupCallPanelSource = .peer(peerId)
|
||||
}
|
||||
|
||||
self.navigationSecondaryContentNode = SparseNode()
|
||||
self.tabsNode = SparseNode()
|
||||
self.tabContainerNode = ChatListFilterTabContainerNode()
|
||||
self.navigationSecondaryContentNode.addSubnode(self.tabContainerNode)
|
||||
self.tabsNode.addSubnode(self.tabContainerNode)
|
||||
|
||||
self.storyListHeight = 0.0
|
||||
|
||||
@ -435,10 +445,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
/*if let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateExpansionProgress(1.0, animated: true)
|
||||
}*/
|
||||
//TODO:scroll to top
|
||||
strongSelf.chatListDisplayNode.willScrollToTop()
|
||||
strongSelf.chatListDisplayNode.scrollToTop()
|
||||
}
|
||||
}
|
||||
@ -451,10 +458,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
} else {
|
||||
switch strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.visibleContentOffset() {
|
||||
case .none, .unknown:
|
||||
//TODO:scroll to top
|
||||
/*if let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateExpansionProgress(1.0, animated: true)
|
||||
}*/
|
||||
strongSelf.chatListDisplayNode.willScrollToTop()
|
||||
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top)
|
||||
case let .known(offset):
|
||||
let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.mainContainerNode.availableFilters.first?.filter
|
||||
@ -472,10 +476,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
strongSelf.selectTab(id: targetTab)
|
||||
} else {
|
||||
//TODO:scroll to top
|
||||
/*if let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateExpansionProgress(1.0, animated: true)
|
||||
}*/
|
||||
strongSelf.chatListDisplayNode.willScrollToTop()
|
||||
if let inlineStackContainerNode = strongSelf.chatListDisplayNode.inlineStackContainerNode {
|
||||
inlineStackContainerNode.currentItemNode.scrollToPosition(.top)
|
||||
} else {
|
||||
@ -781,19 +782,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
)))
|
||||
]))
|
||||
|
||||
let _ = contentComponent
|
||||
//TODO:download indicator
|
||||
/*strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
|
||||
content: contentComponent,
|
||||
action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if let navigationBarView = strongSelf.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarView.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
|
||||
content: contentComponent,
|
||||
action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activateSearch(filter: .downloads, query: nil)
|
||||
}
|
||||
strongSelf.activateSearch(filter: .downloads, query: nil)
|
||||
}
|
||||
)))*/
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
//strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil)
|
||||
if let navigationBarView = strongSelf.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarView.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1254,40 +1257,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
navigationController.filterController(strongSelf, animated: true)
|
||||
}
|
||||
|
||||
/*self.chatListDisplayNode.contentOffsetChanged = { [weak self] offset in
|
||||
if let strongSelf = self, let validLayout = strongSelf.validLayout {
|
||||
var offset = offset
|
||||
if validLayout.inVoiceOver {
|
||||
offset = .known(0.0)
|
||||
}
|
||||
|
||||
let offsetValue: CGFloat
|
||||
switch offset {
|
||||
case .none:
|
||||
offsetValue = 0.0
|
||||
case let .known(value):
|
||||
offsetValue = value
|
||||
case .unknown:
|
||||
offsetValue = 10000.0
|
||||
}
|
||||
|
||||
if let navigationBarView = strongSelf.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarView.applyScroll(offset: offsetValue, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
self.chatListDisplayNode.contentScrollingEnded = { [weak self] listView in
|
||||
let _ = self
|
||||
/*if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
|
||||
return fixListNodeScrolling(listView, searchNode: searchContentNode)
|
||||
} else {
|
||||
return false
|
||||
}*/
|
||||
//TODO:fix scrolling
|
||||
return false
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.emptyListAction = { [weak self] _ in
|
||||
guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else {
|
||||
return
|
||||
@ -2426,133 +2395,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
return (primaryContent, secondaryContent)
|
||||
|
||||
/*let storiesFraction: CGFloat = 0.0
|
||||
//TODO:move to navigation bar
|
||||
/*if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
|
||||
if self.storyListHeight > 0.0 {
|
||||
let fraction = navigationBarSearchContentHeight / searchContentNode.nominalHeight
|
||||
|
||||
let fromLow: CGFloat = fraction
|
||||
let toLow: CGFloat = 0.0
|
||||
let fromHigh: CGFloat = 1.0
|
||||
let toHigh: CGFloat = 1.0
|
||||
let visibleProgress: CGFloat = toLow + (searchContentNode.expansionProgress - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
|
||||
storiesFraction = max(0.0, min(1.0, visibleProgress))
|
||||
}
|
||||
}*/
|
||||
|
||||
let _ = self.headerContentView.update(
|
||||
transition: Transition(transition),
|
||||
component: AnyComponent(ChatListHeaderComponent(
|
||||
sideInset: layout.safeInsets.left + 16.0,
|
||||
primaryContent: primaryContent,
|
||||
secondaryContent: secondaryContent,
|
||||
secondaryTransition: self.chatListDisplayNode.inlineStackContainerTransitionFraction,
|
||||
networkStatus: nil,
|
||||
storySubscriptions: self.storySubscriptions,
|
||||
storiesFraction: storiesFraction,
|
||||
context: self.context,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
openStatusSetup: { [weak self] sourceView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openStatusSetup(sourceView: sourceView)
|
||||
},
|
||||
toggleIsLocked: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.appLockContext.lock()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 44.0)
|
||||
)
|
||||
if let componentView = self.headerContentView.view as? NavigationBarHeaderView {
|
||||
if self.navigationBar?.customHeaderContentView !== componentView {
|
||||
self.navigationBar?.customHeaderContentView = componentView
|
||||
}
|
||||
}
|
||||
if case .chatList(.root) = self.location {
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
componentView.storyPeerAction = { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let storyContent = StoryContentContextImpl(context: self.context, focusedPeerId: peer?.id)
|
||||
let _ = (storyContent.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil {
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
if let peer, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
content: storyContent,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { [weak self] peerId, _ in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||
destinationIsAvatar: true,
|
||||
completed: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
)
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@ -2569,17 +2411,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
//TODO:move to chat list node
|
||||
/*if case .chatList(.root) = self.location, !self.isSearchActive {
|
||||
self.searchContentNode?.additionalHeight = (1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction) * self.storyListHeight
|
||||
}*/
|
||||
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
if let searchTabsNode = self.searchTabsNode {
|
||||
searchTabsNode.bounds.origin = CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + navigationBarSearchContentHeight + 44.0)
|
||||
}
|
||||
|
||||
self.updateLayout(layout: layout, transition: transition)
|
||||
|
||||
if layout.inVoiceOver != wasInVoiceOver {
|
||||
@ -2613,7 +2454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
|
||||
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: { [weak self] _ in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
@ -2721,12 +2562,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
tabContainerOffset += 44.0 + 20.0
|
||||
}
|
||||
|
||||
let navigationBarHeight: CGFloat = 100.0//self.navigationBar?.frame.maxY ?? 0.0
|
||||
let secondaryContentHeight = self.navigationBar?.secondaryContentHeight ?? 0.0
|
||||
let navigationBarHeight: CGFloat = 0.0//self.navigationBar?.frame.maxY ?? 0.0
|
||||
//let secondaryContentHeight = self.navigationBar?.secondaryContentHeight ?? 0.0
|
||||
|
||||
transition.updateFrame(node: self.navigationSecondaryContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - secondaryContentHeight + tabContainerOffset), size: CGSize(width: layout.size.width, height: secondaryContentHeight)))
|
||||
|
||||
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: layout.size.width, height: 46.0)))
|
||||
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 46.0)))
|
||||
|
||||
if !skipTabContainerUpdate {
|
||||
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
@ -3175,11 +3014,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
let animated = strongSelf.didSetupTabs
|
||||
strongSelf.didSetupTabs = true
|
||||
|
||||
if strongSelf.displayNavigationBar {
|
||||
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false)
|
||||
}
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
if wasEmpty != isEmpty {
|
||||
@ -3557,9 +3391,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
let _ = tabsIsEmpty
|
||||
//TODO:swap tabs
|
||||
|
||||
let displaySearchFilters = true
|
||||
if !tabsIsEmpty, let snapshotView = strongSelf.tabContainerNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
/*if !tabsIsEmpty, let snapshotView = strongSelf.tabContainerNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = strongSelf.navigationSecondaryContentNode.frame
|
||||
strongSelf.navigationSecondaryContentNode.view.superview?.addSubview(snapshotView)
|
||||
|
||||
@ -3567,14 +3403,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -strongSelf.storyListHeight), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
}*/
|
||||
|
||||
if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
|
||||
let (filterContainerNode, activate) = filterContainerNodeAndActivate
|
||||
if displaySearchFilters {
|
||||
//TODO:search filters
|
||||
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
||||
let searchTabsNode = SparseNode()
|
||||
strongSelf.searchTabsNode = searchTabsNode
|
||||
searchTabsNode.addSubnode(filterContainerNode)
|
||||
}
|
||||
|
||||
activate(filter != .downloads)
|
||||
@ -3582,13 +3418,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
|
||||
searchContentNode.search(filter: filter, query: query)
|
||||
}
|
||||
|
||||
let tabsOffset = 30.0 + strongSelf.storyListHeight
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
filterContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: tabsOffset), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
@ -3617,7 +3446,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if !self.displayNavigationBar {
|
||||
var completion: (() -> Void)?
|
||||
|
||||
let tabsIsEmpty: Bool
|
||||
self.searchTabsNode = nil
|
||||
|
||||
/*let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
@ -3627,11 +3458,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
var filterContainerNode: ASDisplayNode?
|
||||
|
||||
var searchContentNode: NavigationBarSearchContentNode?
|
||||
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
searchContentNode = navigationBarView.searchContentNode
|
||||
}
|
||||
|
||||
if animated, let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
|
||||
filterContainerNode = searchContentNode.filterContainerNode
|
||||
|
||||
@ -3651,6 +3477,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
var searchContentNode: NavigationBarSearchContentNode?
|
||||
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
searchContentNode = navigationBarView.searchContentNode
|
||||
}
|
||||
|
||||
if let searchContentNode {
|
||||
@ -3664,15 +3495,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
self.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
//TODO:move layout to navigation bar
|
||||
/*if case .chatList(.root) = self.location {
|
||||
self.searchContentNode?.additionalHeight = self.storyListHeight
|
||||
}*/
|
||||
self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false)
|
||||
//TODO:swap tabs
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
transition.updateAlpha(node: self.tabContainerNode, alpha: tabsIsEmpty ? 0.0 : 1.0)
|
||||
//transition.updateAlpha(node: self.tabContainerNode, alpha: tabsIsEmpty ? 0.0 : 1.0)
|
||||
self.setDisplayNavigationBar(true, transition: transition)
|
||||
|
||||
completion?()
|
||||
@ -5049,7 +4875,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if finished, let componentView = self.chatListHeaderView() {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
@ -5659,7 +5485,7 @@ private final class ChatListLocationContext {
|
||||
self.ready.set(.single(true))
|
||||
}
|
||||
|
||||
self.parentController?.requestLayout(transition: .immediate)
|
||||
self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring))
|
||||
}
|
||||
|
||||
private func updateForum(
|
||||
@ -5751,7 +5577,7 @@ private final class ChatListLocationContext {
|
||||
navigationController.replaceController(parentController, with: chatController, animated: true)
|
||||
}
|
||||
} else {
|
||||
self.parentController?.requestLayout(transition: .immediate)
|
||||
self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -748,6 +748,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
private let filterEmptyAction: (ChatListFilter?) -> Void
|
||||
private let secondaryEmptyAction: () -> Void
|
||||
|
||||
fileprivate var onStoriesLockedUpdated: ((Bool) -> Void)?
|
||||
|
||||
fileprivate var onFilterSwitch: (() -> Void)?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@ -770,6 +772,10 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
private var filtersLimit: Int32? = nil
|
||||
private var selectedId: ChatListFilterTabEntryId
|
||||
|
||||
var storiesUnlocked: Bool = false
|
||||
|
||||
var initialScrollingOffset: CGFloat?
|
||||
|
||||
public private(set) var transitionFraction: CGFloat = 0.0
|
||||
private var transitionFractionOffset: CGFloat = 0.0
|
||||
private var disableItemNodeOperationsWhileAnimating: Bool = false
|
||||
@ -806,6 +812,9 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
return self.currentItemNode.chatListFilter.flatMap { .filter($0.id) } ?? .all
|
||||
}
|
||||
|
||||
private var didSetupContentOffset = false
|
||||
private var isSettingUpContentOffset = false
|
||||
|
||||
private func applyItemNodeAsCurrent(id: ChatListFilterTabEntryId, itemNode: ChatListContainerItemNode) {
|
||||
if let previousItemNode = self.currentItemNodeValue {
|
||||
previousItemNode.listNode.activateSearch = nil
|
||||
@ -824,6 +833,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
previousItemNode.listNode.updatePeerGrouping = nil
|
||||
previousItemNode.listNode.contentOffsetChanged = nil
|
||||
previousItemNode.listNode.contentScrollingEnded = nil
|
||||
previousItemNode.listNode.endedInteractiveDragging = { _ in }
|
||||
previousItemNode.listNode.activateChatPreview = nil
|
||||
previousItemNode.listNode.openStories = nil
|
||||
previousItemNode.listNode.addedVisibleChatsWithPeerIds = nil
|
||||
@ -876,15 +886,98 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
itemNode.listNode.updatePeerGrouping = { [weak self] peerId, group in
|
||||
self?.updatePeerGrouping?(peerId, group)
|
||||
}
|
||||
itemNode.listNode.contentOffsetChanged = { [weak self] offset in
|
||||
guard let self else {
|
||||
itemNode.listNode.contentOffsetChanged = { [weak self, weak itemNode] offset in
|
||||
guard let self, let itemNode else {
|
||||
return
|
||||
}
|
||||
if self.isSettingUpContentOffset {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.didSetupContentOffset, let initialScrollingOffset = self.initialScrollingOffset {
|
||||
self.initialScrollingOffset = nil
|
||||
self.didSetupContentOffset = true
|
||||
self.isSettingUpContentOffset = true
|
||||
|
||||
let _ = itemNode.listNode.scrollToOffsetFromTop(initialScrollingOffset, animated: false)
|
||||
|
||||
let offset = itemNode.listNode.visibleContentOffset()
|
||||
self.contentOffset = offset
|
||||
self.contentOffsetChanged?(offset)
|
||||
|
||||
self.isSettingUpContentOffset = false
|
||||
return
|
||||
}
|
||||
self.contentOffset = offset
|
||||
self.contentOffsetChanged?(offset)
|
||||
|
||||
if itemNode.listNode.isTracking {
|
||||
if case let .known(value) = offset {
|
||||
if !self.storiesUnlocked {
|
||||
if value < -1.0 {
|
||||
self.storiesUnlocked = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = true
|
||||
self.onStoriesLockedUpdated?(true)
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
itemNode.listNode.endedInteractiveDragging = { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch self.currentItemNode.visibleContentOffset() {
|
||||
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)
|
||||
let _ = self.contentScrollingEnded?(self.currentItemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
itemNode.listNode.contentScrollingEnded = { [weak self] listView in
|
||||
return self?.contentScrollingEnded?(listView) ?? false
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.contentScrollingEnded?(listView) ?? false
|
||||
}
|
||||
itemNode.listNode.activateChatPreview = { [weak self] item, threadId, sourceNode, gesture, location in
|
||||
self?.activateChatPreview?(item, threadId, sourceNode, gesture, location)
|
||||
@ -1059,8 +1152,15 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
for (id, itemNode) in self.itemNodes {
|
||||
if id != selectedId {
|
||||
itemNode.emptyNode?.restartAnimation()
|
||||
|
||||
if let controller = self.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
|
||||
let scrollOffset = max(0.0, clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight)
|
||||
|
||||
let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let presentationLayer = itemNode.layer.presentation() {
|
||||
self.transitionFraction = presentationLayer.frame.minX / layout.size.width
|
||||
self.transitionFractionOffset = self.transitionFraction
|
||||
@ -1145,6 +1245,9 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
directionIsToRight = translation.x > layout.size.width / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
var applyNodeAsCurrent: ChatListFilterTabEntryId?
|
||||
|
||||
if let directionIsToRight = directionIsToRight {
|
||||
var updatedIndex = selectedIndex
|
||||
if directionIsToRight {
|
||||
@ -1154,15 +1257,15 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
}
|
||||
let switchToId = self.availableFilters[updatedIndex].id
|
||||
if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] {
|
||||
let _ = itemNode
|
||||
self.selectedId = switchToId
|
||||
self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode)
|
||||
applyNodeAsCurrent = switchToId
|
||||
}
|
||||
}
|
||||
self.transitionFraction = 0.0
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
||||
self.disableItemNodeOperationsWhileAnimating = true
|
||||
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: transition)
|
||||
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
|
||||
DispatchQueue.main.async {
|
||||
self.disableItemNodeOperationsWhileAnimating = false
|
||||
if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
|
||||
@ -1171,6 +1274,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
}
|
||||
|
||||
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
|
||||
|
||||
if let switchToId = applyNodeAsCurrent, let itemNode = self.itemNodes[switchToId] {
|
||||
self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode)
|
||||
}
|
||||
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -1602,6 +1710,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
self.mainContainerNode.onStoriesLockedUpdated = { [weak self] isLocked in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
}
|
||||
|
||||
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
|
||||
guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else {
|
||||
return []
|
||||
@ -1711,6 +1826,14 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
private func updateNavigationBar(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
|
||||
let headerContent = self.controller?.updateHeaderContent(layout: layout, transition: transition)
|
||||
|
||||
var tabsNode: ASDisplayNode?
|
||||
|
||||
if let value = self.controller?.searchTabsNode {
|
||||
tabsNode = value
|
||||
} else if let value = self.controller?.tabsNode, self.controller?.hasTabs == true {
|
||||
tabsNode = value
|
||||
}
|
||||
|
||||
let navigationBarSize = self.navigationBarView.update(
|
||||
transition: Transition(transition),
|
||||
component: AnyComponent(ChatListNavigationBar(
|
||||
@ -1720,10 +1843,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
sideInset: layout.safeInsets.left,
|
||||
isSearchActive: self.isSearchDisplayControllerActive,
|
||||
storiesUnlocked: self.mainContainerNode.storiesUnlocked,
|
||||
primaryContent: headerContent?.primaryContent,
|
||||
secondaryContent: headerContent?.secondaryContent,
|
||||
secondaryTransition: self.inlineStackContainerTransitionFraction,
|
||||
storySubscriptions: self.controller?.storySubscriptions,
|
||||
tabsNode: tabsNode,
|
||||
activateSearch: { [weak self] searchContentNode in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
@ -1767,7 +1892,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNavigationScrolling(transition: ContainedViewLayoutTransition) {
|
||||
private func getEffectiveNavigationScrollingOffset() -> CGFloat {
|
||||
let mainOffset: CGFloat
|
||||
if let contentOffset = self.mainContainerNode.contentOffset, case let .known(value) = contentOffset {
|
||||
mainOffset = value
|
||||
@ -1789,8 +1914,17 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
resultingOffset = mainOffset
|
||||
}
|
||||
|
||||
return resultingOffset
|
||||
}
|
||||
|
||||
private func updateNavigationScrolling(transition: ContainedViewLayoutTransition) {
|
||||
var offset = self.getEffectiveNavigationScrollingOffset()
|
||||
if self.isSearchDisplayControllerActive {
|
||||
offset = 0.0
|
||||
}
|
||||
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarComponentView.applyScroll(offset: resultingOffset, transition: Transition(transition))
|
||||
navigationBarComponentView.applyScroll(offset: offset, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1801,6 +1935,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var storiesInset = storiesInset
|
||||
|
||||
let navigationBarLayout = self.updateNavigationBar(layout: layout, transition: transition)
|
||||
self.mainContainerNode.initialScrollingOffset = navigationBarSearchContentHeight + navigationBarLayout.storiesInset
|
||||
|
||||
navigationBarHeight = navigationBarLayout.navigationHeight
|
||||
visualNavigationHeight = navigationBarLayout.navigationHeight
|
||||
@ -2018,6 +2153,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
|
||||
private var contentOffsetSyncLockedIn: Bool = false
|
||||
|
||||
func willScrollToTop() {
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarComponentView.applyScroll(offset: 0.0, transition: Transition(animation: .curve(duration: 0.3, curve: .slide)))
|
||||
}
|
||||
}
|
||||
|
||||
private func contentOffsetChanged(offset: ListViewVisibleContentOffset, isPrimary: Bool) {
|
||||
guard let containerLayout = self.containerLayout else {
|
||||
return
|
||||
@ -2074,7 +2215,45 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool {
|
||||
guard let inlineStackContainerNode = self.inlineStackContainerNode else {
|
||||
if !isPrimary || self.inlineStackContainerNode == nil {
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
|
||||
if navigationBarComponentView.effectiveStoriesInsetHeight > 0.0 {
|
||||
if clippedScrollOffset > 0.0 && clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight {
|
||||
if clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
let searchScrollOffset = clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight
|
||||
if searchScrollOffset > 0.0 && searchScrollOffset < navigationBarSearchContentHeight {
|
||||
if searchScrollOffset < navigationBarSearchContentHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight + navigationBarSearchContentHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if clippedScrollOffset > 0.0 && clippedScrollOffset < navigationBarSearchContentHeight {
|
||||
if clippedScrollOffset < navigationBarSearchContentHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarSearchContentHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
/*guard let inlineStackContainerNode = self.inlineStackContainerNode else {
|
||||
return self.contentScrollingEnded?(listView) ?? false
|
||||
}
|
||||
|
||||
@ -2086,7 +2265,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
|
||||
let _ = inlineStackContainerNode
|
||||
|
||||
return self.contentScrollingEnded?(listView) ?? false
|
||||
return self.contentScrollingEnded?(listView) ?? false*/
|
||||
}
|
||||
|
||||
func makeInlineChatList(location: ChatListControllerLocation) -> ChatListContainerNode {
|
||||
@ -2131,6 +2310,11 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
|
||||
let previousInlineStackContainerNode = self.inlineStackContainerNode
|
||||
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
|
||||
let scrollOffset = max(0.0, clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight)
|
||||
inlineStackContainerNode.initialScrollingOffset = scrollOffset
|
||||
}
|
||||
|
||||
self.inlineStackContainerNode = inlineStackContainerNode
|
||||
self.inlineStackContainerTransitionFraction = 1.0
|
||||
|
||||
|
@ -3173,7 +3173,8 @@ public final class ChatListNode: ListView {
|
||||
var isNavigationInAFinalState: Bool {
|
||||
switch self.visibleContentOffset() {
|
||||
case let .known(value):
|
||||
if value < self.scrollHeightTopInset - 1.0 {
|
||||
let _ = value
|
||||
/*if value < self.scrollHeightTopInset - 1.0 {
|
||||
if abs(value - 0.0) < 1.0 {
|
||||
return true
|
||||
}
|
||||
@ -3181,9 +3182,9 @@ public final class ChatListNode: ListView {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
} else {*/
|
||||
return true
|
||||
}
|
||||
//}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@ -3216,6 +3217,7 @@ public final class ChatListNode: ListView {
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })*/
|
||||
}
|
||||
|
||||
public var ignoreStoryInsetAdjustment: Bool = false
|
||||
private var previousStoriesInset: CGFloat?
|
||||
|
||||
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, visibleTopInset: CGFloat, originalTopInset: CGFloat, storiesInset: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat) {
|
||||
@ -3257,7 +3259,11 @@ public final class ChatListNode: ListView {
|
||||
var additionalScrollDistance: CGFloat = 0.0
|
||||
|
||||
if let previousStoriesInset = self.previousStoriesInset {
|
||||
additionalScrollDistance += previousStoriesInset - storiesInset
|
||||
if self.ignoreStoryInsetAdjustment {
|
||||
//additionalScrollDistance += -20.0
|
||||
} else {
|
||||
additionalScrollDistance += previousStoriesInset - storiesInset
|
||||
}
|
||||
}
|
||||
self.previousStoriesInset = storiesInset
|
||||
//print("storiesInset: \(storiesInset), additionalScrollDistance: \(additionalScrollDistance)")
|
||||
|
@ -84,6 +84,10 @@ public struct Transition {
|
||||
return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset)
|
||||
}
|
||||
}
|
||||
|
||||
public static var slide: Curve {
|
||||
return .custom(0.33, 0.52, 0.25, 0.99)
|
||||
}
|
||||
}
|
||||
|
||||
case none
|
||||
|
@ -1360,7 +1360,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.visibleBottomContentOffsetChanged(self.visibleBottomContentOffset())
|
||||
}
|
||||
|
||||
private func stopScrolling() {
|
||||
public func stopScrolling() {
|
||||
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
|
||||
self.ignoreScrollingEvents = true
|
||||
self.scroller.setContentOffset(self.scroller.contentOffset, animated: false)
|
||||
|
@ -210,26 +210,40 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
} else if innerAlpha < 0.0001 {
|
||||
innerAlpha = 0.0
|
||||
}
|
||||
if !transition.isAnimated {
|
||||
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
}
|
||||
if strongSelf.labelNode.alpha != innerAlpha {
|
||||
if !transition.isAnimated {
|
||||
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
}
|
||||
|
||||
transition.updateAlpha(node: strongSelf.labelNode, alpha: innerAlpha)
|
||||
transition.updateAlpha(node: strongSelf.iconNode, alpha: innerAlpha)
|
||||
}
|
||||
|
||||
let outerAlpha = min(0.3, expansionProgress) / 0.3
|
||||
let cornerRadius = min(strongSelf.fieldStyle.cornerDiameter / 2.0, height / 2.0)
|
||||
if !transition.isAnimated {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "cornerRadius")
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "position")
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "bounds")
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
|
||||
if strongSelf.backgroundNode.cornerRadius != cornerRadius {
|
||||
if !transition.isAnimated {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "cornerRadius")
|
||||
}
|
||||
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.alpha != outerAlpha {
|
||||
if !transition.isAnimated {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
}
|
||||
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.frame != CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)) {
|
||||
if !transition.isAnimated {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "position")
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "bounds")
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)))
|
||||
}
|
||||
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
|
||||
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
|
||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)))
|
||||
|
||||
if let accessoryComponentContainer = strongSelf.accessoryComponentContainer {
|
||||
accessoryComponentContainer.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentContainer.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentContainer.bounds.height) / 2.0)), size: accessoryComponentContainer.bounds.size)
|
||||
|
@ -9,22 +9,26 @@ public enum EngineStoryInputMedia {
|
||||
}
|
||||
|
||||
public struct EngineStoryPrivacy: Equatable {
|
||||
public enum Base {
|
||||
case everyone
|
||||
case contacts
|
||||
case closeFriends
|
||||
case nobody
|
||||
}
|
||||
public typealias Base = Stories.Item.Privacy.Base
|
||||
|
||||
public var base: Base
|
||||
public var additionallyIncludePeers: [EnginePeer.Id]
|
||||
|
||||
public init(base: Base, additionallyIncludePeers: [EnginePeer.Id]) {
|
||||
public init(base: Stories.Item.Privacy.Base, additionallyIncludePeers: [EnginePeer.Id]) {
|
||||
self.base = base
|
||||
self.additionallyIncludePeers = additionallyIncludePeers
|
||||
}
|
||||
}
|
||||
|
||||
public extension EngineStoryPrivacy {
|
||||
init(_ privacy: Stories.Item.Privacy) {
|
||||
self.init(
|
||||
base: privacy.base,
|
||||
additionallyIncludePeers: privacy.additionallyIncludePeers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Stories {
|
||||
public final class Item: Codable, Equatable {
|
||||
public struct Views: Codable, Equatable {
|
||||
@ -100,6 +104,9 @@ public enum Stories {
|
||||
case entities
|
||||
case views
|
||||
case privacy
|
||||
case isPinned
|
||||
case isExpired
|
||||
case isPublic
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
@ -109,6 +116,9 @@ public enum Stories {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let views: Views?
|
||||
public let privacy: Privacy?
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
@ -117,7 +127,10 @@ public enum Stories {
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
views: Views?,
|
||||
privacy: Privacy?
|
||||
privacy: Privacy?,
|
||||
isPinned: Bool,
|
||||
isExpired: Bool,
|
||||
isPublic: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
@ -126,6 +139,9 @@ public enum Stories {
|
||||
self.entities = entities
|
||||
self.views = views
|
||||
self.privacy = privacy
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -144,6 +160,9 @@ public enum Stories {
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.views = try container.decodeIfPresent(Views.self, forKey: .views)
|
||||
self.privacy = try container.decodeIfPresent(Privacy.self, forKey: .privacy)
|
||||
self.isPinned = try container.decodeIfPresent(Bool.self, forKey: .isPinned) ?? false
|
||||
self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false
|
||||
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -163,6 +182,9 @@ public enum Stories {
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encodeIfPresent(self.views, forKey: .views)
|
||||
try container.encodeIfPresent(self.privacy, forKey: .privacy)
|
||||
try container.encode(self.isPinned, forKey: .isPinned)
|
||||
try container.encode(self.isExpired, forKey: .isExpired)
|
||||
try container.encode(self.isPublic, forKey: .isPublic)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
@ -195,6 +217,15 @@ public enum Stories {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpired != rhs.isExpired {
|
||||
return false
|
||||
}
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -698,6 +729,41 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> S
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStoryIsPinned(account: Account, id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: item.privacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
account.stateManager.injectStoryUpdates(updates: [.added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return account.network.request(Api.functions.stories.togglePinned(id: [id], pinned: isPinned ? .boolTrue : .boolFalse))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
extension Api.StoryItem {
|
||||
var id: Int32 {
|
||||
switch self {
|
||||
@ -762,6 +828,10 @@ extension Stories.StoredItem {
|
||||
parsedPrivacy = Stories.Item.Privacy(base: base, additionallyIncludePeers: additionalPeerIds)
|
||||
}
|
||||
|
||||
let isPinned = (flags & (1 << 5)) != 0
|
||||
let isExpired = (flags & (1 << 6)) != 0
|
||||
let isPublic = (flags & (1 << 7)) != 0
|
||||
|
||||
let item = Stories.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
@ -769,7 +839,10 @@ extension Stories.StoredItem {
|
||||
text: caption ?? "",
|
||||
entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [],
|
||||
views: views.flatMap(Stories.Item.Views.init(apiViews:)),
|
||||
privacy: parsedPrivacy
|
||||
privacy: parsedPrivacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: isExpired,
|
||||
isPublic: isPublic
|
||||
)
|
||||
self = .item(item)
|
||||
} else {
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
|
||||
enum InternalStoryUpdate {
|
||||
case deleted(peerId: PeerId, id: Int32)
|
||||
@ -37,8 +38,11 @@ public final class EngineStoryItem: Equatable {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let views: Views?
|
||||
public let privacy: EngineStoryPrivacy?
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
|
||||
public init(id: Int32, timestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?) {
|
||||
public init(id: Int32, timestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
@ -46,6 +50,9 @@ public final class EngineStoryItem: Equatable {
|
||||
self.entities = entities
|
||||
self.views = views
|
||||
self.privacy = privacy
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
@ -70,6 +77,15 @@ public final class EngineStoryItem: Equatable {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpired != rhs.isExpired {
|
||||
return false
|
||||
}
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -255,6 +271,15 @@ public final class StorySubscriptionsContext {
|
||||
var updatedPeerEntries: [StoryItemsTableEntry] = []
|
||||
for story in stories {
|
||||
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
|
||||
/*#if DEBUG
|
||||
if "".isEmpty {
|
||||
if let codedEntry = CodableEntry(Stories.StoredItem.placeholder(Stories.Placeholder(id: storedItem.id, timestamp: storedItem.timestamp))) {
|
||||
updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
#endif*/
|
||||
|
||||
if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) {
|
||||
updatedPeerEntries.append(previousEntry)
|
||||
} else {
|
||||
@ -331,3 +356,275 @@ public final class StorySubscriptionsContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerStoryListContext {
|
||||
public struct State: Equatable {
|
||||
public var peerReference: PeerReference?
|
||||
public var items: [EngineStoryItem]
|
||||
public var totalCount: Int
|
||||
public var loadMoreToken: Int?
|
||||
|
||||
init(
|
||||
peerReference: PeerReference?,
|
||||
items: [EngineStoryItem],
|
||||
totalCount: Int,
|
||||
loadMoreToken: Int?
|
||||
) {
|
||||
self.peerReference = peerReference
|
||||
self.items = items
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
}
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private let peerId: EnginePeer.Id
|
||||
private let isArchived: Bool
|
||||
|
||||
private let statePromise = Promise<State>()
|
||||
private var stateValue: State {
|
||||
didSet {
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
}
|
||||
}
|
||||
public var state: Signal<State, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
private var isLoadingMore: Bool = false
|
||||
private var requestDisposable: Disposable?
|
||||
|
||||
private var updatesDisposable: Disposable?
|
||||
|
||||
public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
self.isArchived = isArchived
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0)
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
guard let loadMoreToken = self.stateValue.loadMoreToken else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingMore = true
|
||||
|
||||
let peerId = self.peerId
|
||||
let account = self.account
|
||||
let isArchived = self.isArchived
|
||||
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .single(([], 0, nil))
|
||||
}
|
||||
|
||||
let signal: Signal<Api.stories.Stories, MTRpcError>
|
||||
if isArchived {
|
||||
signal = account.network.request(Api.functions.stories.getExpiredStories(offsetId: Int32(loadMoreToken), limit: 100))
|
||||
} else {
|
||||
signal = account.network.request(Api.functions.stories.getPinnedStories(userId: inputUser, offsetId: Int32(loadMoreToken), limit: 100))
|
||||
}
|
||||
return signal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
guard let result = result else {
|
||||
return .single(([], 0, nil))
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in
|
||||
var storyItems: [EngineStoryItem] = []
|
||||
var totalCount: Int = 0
|
||||
|
||||
switch result {
|
||||
case let .stories(count, stories, users):
|
||||
totalCount = Int(count)
|
||||
|
||||
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)
|
||||
|
||||
for story in stories {
|
||||
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
|
||||
if case let .item(item) = storedItem, let media = item.media {
|
||||
let mappedItem = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init))
|
||||
}
|
||||
}
|
||||
}).start(next: { [weak self] storyItems, totalCount, peerReference in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingMore = false
|
||||
|
||||
var updatedState = self.stateValue
|
||||
|
||||
var existingIds = Set(updatedState.items.map { $0.id })
|
||||
for item in storyItems {
|
||||
if existingIds.contains(item.id) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(item.id)
|
||||
|
||||
updatedState.items.append(item)
|
||||
}
|
||||
|
||||
if updatedState.peerReference == nil {
|
||||
updatedState.peerReference = peerReference
|
||||
}
|
||||
|
||||
updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init)
|
||||
if updatedState.loadMoreToken != nil {
|
||||
updatedState.totalCount = max(totalCount, updatedState.items.count)
|
||||
} else {
|
||||
updatedState.totalCount = updatedState.items.count
|
||||
}
|
||||
self.stateValue = updatedState
|
||||
|
||||
if self.updatesDisposable == nil {
|
||||
self.updatesDisposable = (self.account.stateManager.storyUpdates
|
||||
|> deliverOnMainQueue).start(next: { [weak self] updates in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
let selfPeerId = self.peerId
|
||||
let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .added(peerId, item):
|
||||
if selfPeerId == peerId {
|
||||
if case let .item(item) = item {
|
||||
if let views = item.views {
|
||||
for id in views.seenPeerIds {
|
||||
if let peer = transaction.getPeer(id) {
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return peers
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .deleted(peerId, id):
|
||||
if self.peerId == peerId {
|
||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
}
|
||||
case let .added(peerId, item):
|
||||
if self.peerId == peerId {
|
||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == item.id }) {
|
||||
if !self.isArchived {
|
||||
if case let .item(item) = item {
|
||||
if item.isPinned {
|
||||
if let media = item.media {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items[index] = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
} else {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .read:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -871,6 +871,10 @@ public extension TelegramEngine {
|
||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
|
||||
}
|
||||
|
||||
public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -16,10 +16,12 @@ public final class ChatListNavigationBar: Component {
|
||||
public let statusBarHeight: CGFloat
|
||||
public let sideInset: CGFloat
|
||||
public let isSearchActive: Bool
|
||||
public let storiesUnlocked: Bool
|
||||
public let primaryContent: ChatListHeaderComponent.Content?
|
||||
public let secondaryContent: ChatListHeaderComponent.Content?
|
||||
public let secondaryTransition: CGFloat
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let tabsNode: ASDisplayNode?
|
||||
public let activateSearch: (NavigationBarSearchContentNode) -> Void
|
||||
public let openStatusSetup: (UIView) -> Void
|
||||
|
||||
@ -30,10 +32,12 @@ public final class ChatListNavigationBar: Component {
|
||||
statusBarHeight: CGFloat,
|
||||
sideInset: CGFloat,
|
||||
isSearchActive: Bool,
|
||||
storiesUnlocked: Bool,
|
||||
primaryContent: ChatListHeaderComponent.Content?,
|
||||
secondaryContent: ChatListHeaderComponent.Content?,
|
||||
secondaryTransition: CGFloat,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
tabsNode: ASDisplayNode?,
|
||||
activateSearch: @escaping (NavigationBarSearchContentNode) -> Void,
|
||||
openStatusSetup: @escaping (UIView) -> Void
|
||||
) {
|
||||
@ -43,10 +47,12 @@ public final class ChatListNavigationBar: Component {
|
||||
self.statusBarHeight = statusBarHeight
|
||||
self.sideInset = sideInset
|
||||
self.isSearchActive = isSearchActive
|
||||
self.storiesUnlocked = storiesUnlocked
|
||||
self.primaryContent = primaryContent
|
||||
self.secondaryContent = secondaryContent
|
||||
self.secondaryTransition = secondaryTransition
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.tabsNode = tabsNode
|
||||
self.activateSearch = activateSearch
|
||||
self.openStatusSetup = openStatusSetup
|
||||
}
|
||||
@ -70,6 +76,9 @@ public final class ChatListNavigationBar: Component {
|
||||
if lhs.isSearchActive != rhs.isSearchActive {
|
||||
return false
|
||||
}
|
||||
if lhs.storiesUnlocked != rhs.storiesUnlocked {
|
||||
return false
|
||||
}
|
||||
if lhs.primaryContent != rhs.primaryContent {
|
||||
return false
|
||||
}
|
||||
@ -82,6 +91,9 @@ public final class ChatListNavigationBar: Component {
|
||||
if lhs.storySubscriptions != rhs.storySubscriptions {
|
||||
return false
|
||||
}
|
||||
if lhs.tabsNode != rhs.tabsNode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -109,13 +121,20 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
private var currentLayout: CurrentLayout?
|
||||
private var rawScrollOffset: CGFloat?
|
||||
private var clippedScrollOffset: CGFloat?
|
||||
public private(set) var clippedScrollOffset: CGFloat?
|
||||
|
||||
public var deferScrollApplication: Bool = false
|
||||
private var hasDeferredScrollOffset: Bool = false
|
||||
|
||||
public private(set) var effectiveStoriesInsetHeight: CGFloat = 0.0
|
||||
|
||||
private var applyScrollFractionAnimator: DisplayLinkAnimator?
|
||||
private var applyScrollFraction: CGFloat = 1.0
|
||||
private var applyScrollStartFraction: CGFloat = 0.0
|
||||
|
||||
private var tabsNode: ASDisplayNode?
|
||||
private weak var disappearingTabsView: UIView?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.separatorLayer = SimpleLayer()
|
||||
@ -148,6 +167,11 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
|
||||
public func applyScroll(offset: CGFloat, transition: Transition) {
|
||||
var transition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
self.rawScrollOffset = offset
|
||||
|
||||
if self.deferScrollApplication {
|
||||
@ -169,7 +193,7 @@ public final class ChatListNavigationBar: Component {
|
||||
let effectiveStoriesOffsetDistance: CGFloat
|
||||
|
||||
var minContentOffset: CGFloat = navigationBarSearchContentHeight
|
||||
if !component.isSearchActive, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
|
||||
if !component.isSearchActive, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, component.storiesUnlocked {
|
||||
effectiveStoriesOffsetDistance = defaultStoriesOffsetDistance * (1.0 - component.secondaryTransition)
|
||||
minContentOffset += effectiveStoriesOffsetDistance
|
||||
} else {
|
||||
@ -185,6 +209,8 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
let visibleSize = CGSize(width: currentLayout.size.width, height: max(0.0, currentLayout.size.height - clippedScrollOffset))
|
||||
|
||||
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)))
|
||||
@ -226,15 +252,22 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
|
||||
let clippedStoriesOffset = max(0.0, min(clippedScrollOffset, defaultStoriesOffsetDistance))
|
||||
let storiesOffsetFraction: CGFloat
|
||||
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
|
||||
var storiesOffsetFraction: CGFloat
|
||||
if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, component.storiesUnlocked {
|
||||
storiesOffsetFraction = clippedStoriesOffset / defaultStoriesOffsetDistance
|
||||
} else {
|
||||
storiesOffsetFraction = 1.0
|
||||
}
|
||||
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
storiesOffsetFraction = self.applyScrollFraction * storiesOffsetFraction + (1.0 - self.applyScrollFraction) * 1.0
|
||||
}
|
||||
|
||||
let searchSize = CGSize(width: currentLayout.size.width, height: navigationBarSearchContentHeight)
|
||||
let searchFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 0.0 - searchSize.height), size: searchSize)
|
||||
var searchFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - searchSize.height), size: searchSize)
|
||||
if component.tabsNode != nil {
|
||||
searchFrame.origin.y -= 46.0
|
||||
}
|
||||
|
||||
let clippedSearchOffset = max(0.0, min(clippedScrollOffset - effectiveStoriesOffsetDistance, searchOffsetDistance))
|
||||
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
|
||||
@ -289,11 +322,55 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
transition.setFrame(view: headerContentView, frame: headerContentFrame)
|
||||
}
|
||||
|
||||
if component.tabsNode !== self.tabsNode {
|
||||
if let tabsNode = self.tabsNode {
|
||||
self.tabsNode = nil
|
||||
let disappearingTabsView = tabsNode.view
|
||||
self.disappearingTabsView = disappearingTabsView
|
||||
transition.setAlpha(view: tabsNode.view, alpha: 0.0, completion: { [weak self, weak disappearingTabsView] _ in
|
||||
guard let self, let disappearingTabsView else {
|
||||
return
|
||||
}
|
||||
if self.tabsNode?.view !== disappearingTabsView {
|
||||
disappearingTabsView.removeFromSuperview()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if let tabsNode = component.tabsNode {
|
||||
self.tabsNode = tabsNode
|
||||
|
||||
var tabsNodeTransition = transition
|
||||
if tabsNode.view.superview !== self {
|
||||
tabsNodeTransition = .immediate
|
||||
self.addSubview(tabsNode.view)
|
||||
if !transition.animation.isImmediate {
|
||||
tabsNode.view.alpha = 1.0
|
||||
tabsNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
transition.animatePosition(view: tabsNode.view, from: CGPoint(x: 0.0, y: previousHeight - visibleSize.height), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
tabsNodeTransition.setFrame(view: tabsNode.view, frame: tabsFrame)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var storiesUnlockedUpdated = false
|
||||
if let previousComponent = self.component, previousComponent.storiesUnlocked != component.storiesUnlocked {
|
||||
storiesUnlockedUpdated = true
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@ -315,7 +392,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
self.effectiveStoriesInsetHeight = 0.0
|
||||
} else {
|
||||
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
|
||||
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, component.storiesUnlocked {
|
||||
let storiesHeight: CGFloat = 94.0 * (1.0 - component.secondaryTransition)
|
||||
contentHeight += storiesHeight
|
||||
self.effectiveStoriesInsetHeight = storiesHeight
|
||||
@ -326,11 +403,37 @@ public final class ChatListNavigationBar: Component {
|
||||
contentHeight += navigationBarSearchContentHeight
|
||||
}
|
||||
|
||||
if component.tabsNode != nil {
|
||||
contentHeight += 46.0
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: contentHeight)
|
||||
self.currentLayout = CurrentLayout(size: size)
|
||||
|
||||
self.hasDeferredScrollOffset = true
|
||||
|
||||
if storiesUnlockedUpdated {
|
||||
self.applyScrollFraction = 0.0
|
||||
self.applyScrollStartFraction = 0.0
|
||||
self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: 0.3, from: 0.0, to: 1.0, update: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.applyScrollFraction = listViewAnimationCurveSystem(value)
|
||||
|
||||
if let rawScrollOffset = self.rawScrollOffset {
|
||||
self.hasDeferredScrollOffset = true
|
||||
self.applyScroll(offset: rawScrollOffset, transition: transition)
|
||||
}
|
||||
}, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.applyScrollFractionAnimator?.invalidate()
|
||||
self.applyScrollFractionAnimator = nil
|
||||
})
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
@ -738,7 +738,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
}
|
||||
}
|
||||
|
||||
/*public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
public enum ContentType {
|
||||
case photoOrVideo
|
||||
case photo
|
||||
@ -815,7 +815,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
private var animationTimer: SwiftSignalKit.Timer?
|
||||
|
||||
public private(set) var calendarSource: SparseMessageCalendar?
|
||||
private var listSource: StorySubscriptionsContext
|
||||
private var listSource: PeerStoryListContext
|
||||
|
||||
public var openCurrentDate: (() -> Void)?
|
||||
public var paneDidScroll: (() -> Void)?
|
||||
@ -848,7 +848,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
captureProtected: captureProtected
|
||||
)
|
||||
|
||||
//self.listSource = context.engine.messages.allStories()
|
||||
self.listSource = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
|
||||
self.calendarSource = nil
|
||||
|
||||
super.init()
|
||||
@ -888,6 +888,75 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
}
|
||||
strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue)*/
|
||||
} else {
|
||||
let listContext = PeerStoryListContentContextImpl(
|
||||
context: self.context,
|
||||
peerId: self.peerId,
|
||||
listContext: self.listSource,
|
||||
initialId: item.story.id
|
||||
)
|
||||
let _ = (listContext.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let navigationController = self.chatControllerInteraction.navigationController() else {
|
||||
return
|
||||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
|
||||
let story = item.story
|
||||
var foundItemLayer: SparseItemGridLayer?
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return
|
||||
}
|
||||
if let listItem = itemLayer.item, listItem.story.id == story.id {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
if let foundItemLayer {
|
||||
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: self.view,
|
||||
sourceRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
||||
sourceCornerRadius: 0.0
|
||||
)
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
content: listContext,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { [weak self] _, itemId in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var foundItemLayer: SparseItemGridLayer?
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return
|
||||
}
|
||||
if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
if let foundItemLayer {
|
||||
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: self.view,
|
||||
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
||||
destinationCornerRadius: 0.0,
|
||||
destinationIsAvatar: false,
|
||||
completed: {}
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
)
|
||||
navigationController.pushViewController(storyContainerScreen)
|
||||
})
|
||||
|
||||
/*let _ = (StoryChatContent.stories(
|
||||
context: self.context,
|
||||
storyList: self.listSource,
|
||||
@ -1389,7 +1458,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
let queue = Queue()
|
||||
|
||||
self.listDisposable.set((self.listSource.state
|
||||
|> deliverOn(queue)).start(next: { [weak self] list in
|
||||
|> deliverOn(queue)).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -1399,8 +1468,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
var mappedItems: [SparseItemGrid.Item] = []
|
||||
let mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
||||
var totalCount: Int = 0
|
||||
if let itemSet = list.itemSets.first(where: { $0.peerId == self.peerId }), let peer = itemSet.peer, let peerReference = PeerReference(peer._asPeer()) {
|
||||
for item in itemSet.items {
|
||||
if let peerReference = state.peerReference {
|
||||
for item in state.items {
|
||||
mappedItems.append(VisualMediaItem(
|
||||
index: mappedItems.count,
|
||||
peer: peerReference,
|
||||
@ -1408,9 +1477,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue
|
||||
))
|
||||
}
|
||||
totalCount = itemSet.totalCount ?? mappedItems.count
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
}
|
||||
totalCount = state.totalCount
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -1933,4 +2002,3 @@ private class MediaListSelectionRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -432,11 +432,15 @@ private final class StoryContainerScreenComponent: Component {
|
||||
var update = false
|
||||
self.contentUpdatedDisposable = (component.content.updated
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if update {
|
||||
self.state?.updated(transition: .immediate)
|
||||
if component.content.stateValue?.slice == nil {
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
update = true
|
||||
|
@ -135,7 +135,7 @@ public final class StoryContentContextState {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.item.id != rhs.item.id {
|
||||
if lhs.item.storyItem != rhs.item.storyItem {
|
||||
return false
|
||||
}
|
||||
if lhs.totalCount != rhs.totalCount {
|
||||
|
@ -932,11 +932,39 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = controller
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
/*var items: [ContextMenuItem] = []
|
||||
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue("Everyone"), icon: { theme in
|
||||
let privacyText: String
|
||||
switch component.slice.item.storyItem.privacy?.base {
|
||||
case .closeFriends:
|
||||
if additionalCount != 0 {
|
||||
privacyText = "Close Friends (+\(additionalCount)"
|
||||
} else {
|
||||
privacyText = "Close Friends"
|
||||
}
|
||||
case .contacts:
|
||||
if additionalCount != 0 {
|
||||
privacyText = "Contacts (+\(additionalCount)"
|
||||
} else {
|
||||
privacyText = "Contacts"
|
||||
}
|
||||
case .nobody:
|
||||
if additionalCount != 0 {
|
||||
if additionalCount == 1 {
|
||||
privacyText = "\(additionalCount) Person"
|
||||
} else {
|
||||
privacyText = "\(additionalCount) People"
|
||||
}
|
||||
} else {
|
||||
privacyText = "Only Me"
|
||||
}
|
||||
default:
|
||||
privacyText = "Everyone"
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
@ -949,38 +977,62 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Save to profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
|
||||
component.controller()?.forEachController { c in
|
||||
if let c = c as? UndoOverlayController {
|
||||
c.dismiss()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
self.component?.presentController(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
))
|
||||
|
||||
let _ = component.context.engine.messages.updateStoryIsPinned(id: component.slice.item.storyItem.id, isPinned: !component.slice.item.storyItem.isPinned).start()
|
||||
|
||||
if component.slice.item.storyItem.isPinned {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
self.component?.presentController(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, text: "Story removed from your profile", timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
))
|
||||
} else {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
self.component?.presentController(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
))
|
||||
}
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
})))
|
||||
|
||||
if component.slice.item.storyItem.isPublic {
|
||||
items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
@ -993,7 +1045,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.contextController = contextController
|
||||
self.updateIsProgressPaused()
|
||||
controller.present(contextController, in: .window(.root))*/
|
||||
controller.present(contextController, in: .window(.root))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -1479,7 +1531,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
if let inlineActionsView = self.inlineActions.view {
|
||||
if inlineActionsView.superview == nil {
|
||||
self.contentContainerView.addSubview(inlineActionsView)
|
||||
//self.contentContainerView.addSubview(inlineActionsView)
|
||||
}
|
||||
transition.setFrame(view: inlineActionsView, frame: CGRect(origin: CGPoint(x: contentFrame.width - 10.0 - inlineActionsSize.width, y: contentFrame.height - 20.0 - inlineActionsSize.height), size: inlineActionsSize))
|
||||
|
||||
|
@ -124,11 +124,14 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
var loadKeys: [StoryKey] = []
|
||||
for index in (focusedIndex - 2) ... (focusedIndex + 2) {
|
||||
if index >= 0 && index < itemsView.items.count {
|
||||
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case .placeholder = item {
|
||||
if let item = itemsView.items[index].value.get(Stories.StoredItem.self), case .placeholder = item {
|
||||
loadKeys.append(StoryKey(peerId: peerId, id: item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
if !loadKeys.isEmpty {
|
||||
loadIds(loadKeys)
|
||||
}
|
||||
|
||||
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||
let mappedItem = EngineStoryItem(
|
||||
@ -145,7 +148,10 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: nil
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
|
||||
self.sliceValue = StoryContentContextState.FocusedSlice(
|
||||
@ -584,7 +590,10 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: nil
|
||||
privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: itemValue.isPinned,
|
||||
isExpired: itemValue.isExpired,
|
||||
isPublic: itemValue.isPublic
|
||||
)
|
||||
|
||||
let stateValue = StoryContentContextState(
|
||||
@ -666,3 +675,168 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
private let context: AccountContext
|
||||
|
||||
public private(set) var stateValue: StoryContentContextState?
|
||||
public var state: Signal<StoryContentContextState, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
private let statePromise = Promise<StoryContentContextState>()
|
||||
|
||||
private let updatedPromise = Promise<Void>()
|
||||
public var updated: Signal<Void, NoError> {
|
||||
return self.updatedPromise.get()
|
||||
}
|
||||
|
||||
private var storyDisposable: Disposable?
|
||||
|
||||
private var requestedStoryKeys = Set<StoryKey>()
|
||||
private var requestStoryDisposables = DisposableSet()
|
||||
|
||||
private var listState: PeerStoryListContext.State?
|
||||
|
||||
private var focusedId: Int32?
|
||||
private var focusedIdUpdated = Promise<Void>(Void())
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) {
|
||||
self.context = context
|
||||
|
||||
self.storyDisposable = (combineLatest(queue: .mainQueue(),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
|
||||
listContext.state,
|
||||
self.focusedIdUpdated.get()
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer, state, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.listState = state
|
||||
|
||||
let focusedIndex: Int?
|
||||
if let current = self.focusedId {
|
||||
if let index = state.items.firstIndex(where: { $0.id == current }) {
|
||||
focusedIndex = index
|
||||
} else if let index = state.items.firstIndex(where: { $0.id >= current }) {
|
||||
focusedIndex = index
|
||||
} else if !state.items.isEmpty {
|
||||
focusedIndex = 0
|
||||
} else {
|
||||
focusedIndex = nil
|
||||
}
|
||||
} else if let initialId = initialId {
|
||||
if let index = state.items.firstIndex(where: { $0.id == initialId }) {
|
||||
focusedIndex = index
|
||||
} else if let index = state.items.firstIndex(where: { $0.id >= initialId }) {
|
||||
focusedIndex = index
|
||||
} else {
|
||||
focusedIndex = nil
|
||||
}
|
||||
} else {
|
||||
if !state.items.isEmpty {
|
||||
focusedIndex = 0
|
||||
} else {
|
||||
focusedIndex = nil
|
||||
}
|
||||
}
|
||||
|
||||
let stateValue: StoryContentContextState
|
||||
if let focusedIndex = focusedIndex, let peer = peer {
|
||||
let item = state.items[focusedIndex]
|
||||
self.focusedId = item.id
|
||||
|
||||
stateValue = StoryContentContextState(
|
||||
slice: StoryContentContextState.FocusedSlice(
|
||||
peer: peer,
|
||||
item: StoryContentItem(
|
||||
id: AnyHashable(item.id),
|
||||
position: focusedIndex,
|
||||
component: AnyComponent(StoryItemContentComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
item: item
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
timestamp: item.timestamp
|
||||
)),
|
||||
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
peer: peer
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: item,
|
||||
preload: nil,
|
||||
delete: {
|
||||
},
|
||||
markAsSeen: {
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peerId == self.context.account.peerId
|
||||
),
|
||||
totalCount: state.totalCount,
|
||||
previousItemId: focusedIndex == 0 ? nil : state.items[focusedIndex - 1].id,
|
||||
nextItemId: (focusedIndex == state.items.count - 1) ? nil : state.items[focusedIndex + 1].id
|
||||
),
|
||||
previousSlice: nil,
|
||||
nextSlice: nil
|
||||
)
|
||||
} else {
|
||||
self.focusedId = nil
|
||||
|
||||
stateValue = StoryContentContextState(
|
||||
slice: nil,
|
||||
previousSlice: nil,
|
||||
nextSlice: nil
|
||||
)
|
||||
}
|
||||
|
||||
if self.stateValue == nil || self.stateValue?.slice != stateValue.slice {
|
||||
self.stateValue = stateValue
|
||||
self.statePromise.set(.single(stateValue))
|
||||
self.updatedPromise.set(.single(Void()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.storyDisposable?.dispose()
|
||||
self.requestStoryDisposables.dispose()
|
||||
}
|
||||
|
||||
public func resetSideStates() {
|
||||
}
|
||||
|
||||
public func navigate(navigation: StoryContentContextNavigation) {
|
||||
switch navigation {
|
||||
case .peer:
|
||||
break
|
||||
case let .item(direction):
|
||||
let indexDifference: Int
|
||||
switch direction {
|
||||
case .next:
|
||||
indexDifference = 1
|
||||
case .previous:
|
||||
indexDifference = -1
|
||||
}
|
||||
|
||||
if let listState = self.listState, let focusedId = self.focusedId {
|
||||
if let index = listState.items.firstIndex(where: { $0.id == focusedId }) {
|
||||
var nextIndex = index + indexDifference
|
||||
if nextIndex < 0 {
|
||||
nextIndex = 0
|
||||
}
|
||||
if nextIndex > listState.items.count - 1 {
|
||||
nextIndex = listState.items.count - 1
|
||||
}
|
||||
if nextIndex != index {
|
||||
self.focusedId = listState.items[nextIndex].id
|
||||
self.focusedIdUpdated.set(.single(Void()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -363,11 +363,18 @@ final class StoryItemContentComponent: Component {
|
||||
case let .file(file):
|
||||
self.contentLoaded = true
|
||||
|
||||
signal = chatMessageVideo(
|
||||
signal = mediaGridMessageVideo(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .other,
|
||||
videoReference: .story(peer: peerReference, id: component.item.id, media: file),
|
||||
synchronousLoad: true
|
||||
onlyFullSize: false,
|
||||
useLargeThumbnail: true,
|
||||
synchronousLoad: true,
|
||||
autoFetchFullSizeThumbnail: true,
|
||||
overlayColor: nil,
|
||||
nilForEmptyResult: false,
|
||||
useMiniThumbnailIfAvailable: false,
|
||||
blurred: false
|
||||
)
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: component.context.account.postbox.mediaBox,
|
||||
@ -397,6 +404,10 @@ final class StoryItemContentComponent: Component {
|
||||
wasSynchronous = false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
self.performActionAfterImageContentLoaded(update: false)
|
||||
#endif
|
||||
|
||||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
if let fetchSignal {
|
||||
|
@ -368,8 +368,7 @@ private final class PeerInfoPendingPane {
|
||||
let paneNode: PeerInfoPaneNode
|
||||
switch key {
|
||||
case .stories:
|
||||
//let visualPaneNode = PeerInfoStoryPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected)
|
||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected)
|
||||
let visualPaneNode = PeerInfoStoryPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected)
|
||||
paneNode = visualPaneNode
|
||||
visualPaneNode.openCurrentDate = {
|
||||
openMediaCalendar()
|
||||
|
Loading…
x
Reference in New Issue
Block a user