[WIP] Stories

This commit is contained in:
Ali 2023-05-31 00:38:08 +04:00
parent 5a5adfb5b3
commit 82f511c8a5
17 changed files with 1136 additions and 317 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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 {
}
}
}
*/

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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