diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5bd26329fa..4de106551b 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -326,7 +326,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController switch strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.visibleContentOffset() { case .none, .unknown: strongSelf.chatListDisplayNode.willScrollToTop() - strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false)) case let .known(offset): let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.mainContainerNode.availableFilters.first?.filter @@ -349,9 +349,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.chatListDisplayNode.willScrollToTop() if let inlineStackContainerNode = strongSelf.chatListDisplayNode.inlineStackContainerNode { - inlineStackContainerNode.currentItemNode.scrollToPosition(.top) + inlineStackContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false)) } else { - strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false)) } } } @@ -2404,7 +2404,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer?.id, singlePeer: false) + guard let peer else { + self.chatListDisplayNode.scrollToStories(animated: true) + return + } + + let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] storyContentState in @@ -2412,13 +2417,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil { + if peer.id == self.context.account.peerId, storyContentState.slice == nil { self.openStoryCamera() return } var transitionIn: StoryContainerScreen.TransitionIn? - if let peer, let componentView = self.chatListHeaderView() { + if let componentView = self.chatListHeaderView() { if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, @@ -2594,7 +2599,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } public func scrollToStories() { - self.chatListDisplayNode.scrollToStories() + self.chatListDisplayNode.scrollToStories(animated: false) } private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -3423,9 +3428,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - if let scrollToTop = strongSelf.scrollToTop { + /*if let scrollToTop = strongSelf.scrollToTop { scrollToTop() - } + }*/ let tabsIsEmpty: Bool if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 63e90da60b..508790c8b4 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -361,7 +361,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)? - init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) { + init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, autoSetReady: Bool) { self.context = context self.controller = controller self.location = location @@ -373,7 +373,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { self.secondaryEmptyAction = secondaryEmptyAction self.isInlineMode = isInlineMode - self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode) + self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady) if let controller, case .chatList(groupId: .root) = controller.location { self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight @@ -772,8 +772,20 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele private var filtersLimit: Int32? = nil private var selectedId: ChatListFilterTabEntryId - var storiesUnlocked: Bool = false + var hintUpdatedStoryExpansion: Bool = false var ignoreStoryUnlockedScrolling: Bool = false + var tempTopInset: CGFloat = 0.0 { + didSet { + if self.tempTopInset != oldValue { + for (_, itemNode) in self.itemNodes { + itemNode.listNode.tempTopInset = self.tempTopInset + } + if let pendingItemNode = self.pendingItemNode { + pendingItemNode.1.listNode.tempTopInset = self.tempTopInset + } + } + } + } var initialScrollingOffset: CGFloat? @@ -835,6 +847,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele previousItemNode.listNode.contentOffsetChanged = nil previousItemNode.listNode.contentScrollingEnded = nil previousItemNode.listNode.endedInteractiveDragging = { _ in } + previousItemNode.listNode.shouldStopScrolling = nil previousItemNode.listNode.activateChatPreview = nil previousItemNode.listNode.openStories = nil previousItemNode.listNode.addedVisibleChatsWithPeerIds = nil @@ -910,64 +923,67 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self.isSettingUpContentOffset = false return } - self.contentOffset = offset - self.contentOffsetChanged?(offset) - if itemNode.listNode.isTracking { + if itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 { if case let .known(value) = offset { - if !self.storiesUnlocked { - if value < -40.0 { - self.storiesUnlocked = true - DispatchQueue.main.async { [weak self] in - guard let self else { - return - } - - HapticFeedback().impact() - - self.currentItemNode.ignoreStoryInsetAdjustment = true - self.currentItemNode.allowInsetFixWhileTracking = true - self.onStoriesLockedUpdated?(true) - self.currentItemNode.ignoreStoryInsetAdjustment = false - self.currentItemNode.allowInsetFixWhileTracking = false - } + if value < -1.0 { + if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + self.currentItemNode.startedScrollingAtUpperBound = true + self.tempTopInset = ChatListNavigationBar.storiesScrollHeight } } } - } else if self.storiesUnlocked { - switch offset { - case let .known(value): - if value >= ChatListNavigationBar.storiesScrollHeight && !self.ignoreStoryUnlockedScrolling { - self.storiesUnlocked = false - self.onStoriesLockedUpdated?(false) + } + + self.contentOffset = offset + self.contentOffsetChanged?(offset) + + if self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset != 0.0 { + if case let .known(value) = offset { + if value > 4.0 { + self.currentItemNode.startedScrollingAtUpperBound = false + self.tempTopInset = 0.0 + } else if value <= -94.0 { + } else if value > -82.0 { } - default: - break + } else if case .unknown = offset { + self.currentItemNode.startedScrollingAtUpperBound = false + self.tempTopInset = 0.0 } } } + itemNode.listNode.didBeginInteractiveDragging = { [weak self] _ in + guard let self else { + return + } + let tempTopInset: CGFloat + if self.currentItemNode.startedScrollingAtUpperBound { + if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + tempTopInset = ChatListNavigationBar.storiesScrollHeight + } else { + tempTopInset = 0.0 + } + } else { + tempTopInset = 0.0 + } + if self.tempTopInset != tempTopInset { + self.tempTopInset = tempTopInset + self.hintUpdatedStoryExpansion = true + self.currentItemNode.contentOffsetChanged?(self.currentItemNode.visibleContentOffset()) + self.hintUpdatedStoryExpansion = false + } + } itemNode.listNode.endedInteractiveDragging = { [weak self] _ in guard let self else { return } - switch self.currentItemNode.visibleContentOffset() { - case let .known(value): - if value > ChatListNavigationBar.storiesScrollHeight { - if self.storiesUnlocked { - self.storiesUnlocked = false - - DispatchQueue.main.async { [weak self] in - guard let self else { - return - } - self.onStoriesLockedUpdated?(false) - let _ = self.contentScrollingEnded?(self.currentItemNode) - } - } - } - default: - break + self.endedInteractiveDragging?(self.currentItemNode) + } + itemNode.listNode.shouldStopScrolling = { [weak self] velocity in + guard let self else { + return false } + return self.shouldStopScrolling?(self.currentItemNode, velocity) ?? false } itemNode.listNode.contentScrollingEnded = { [weak self] listView in guard let self else { @@ -975,6 +991,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } return self.contentScrollingEnded?(listView) ?? false + //DispatchQueue.main.async { [weak self] in + // let _ = self?.contentScrollingEnded?(listView) + //} + + //return false } itemNode.listNode.activateChatPreview = { [weak self] item, threadId, sourceNode, gesture, location in self?.activateChatPreview?(item, threadId, sourceNode, gesture, location) @@ -1051,6 +1072,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele var contentOffset: ListViewVisibleContentOffset? public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? + var endedInteractiveDragging: ((ListView) -> Void)? + var shouldStopScrolling: ((ListView, CGFloat) -> Bool)? var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? var openStories: ((EnginePeer.Id, ASDisplayNode?) -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? @@ -1090,7 +1113,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.filterEmptyAction(filter) }, secondaryEmptyAction: { [weak self] in self?.secondaryEmptyAction() - }) + }, autoSetReady: true) self.itemNodes[.all] = itemNode self.addSubnode(itemNode) @@ -1104,7 +1127,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } switch strongSelf.currentItemNode.visibleContentOffset() { case let .known(value): - if value < -1.0 { + if value < -strongSelf.currentItemNode.tempTopInset { return [] } case .none, .unknown: @@ -1158,7 +1181,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele 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 scrollOffset = clippedScrollOffset let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false) } @@ -1336,9 +1359,9 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } } - public func scrollToTop(animated: Bool) { + public func scrollToTop(animated: Bool, adjustForTempInset: Bool) { if let itemNode = self.itemNodes[self.selectedId] { - itemNode.listNode.scrollToPosition(.top, animated: animated) + itemNode.listNode.scrollToPosition(.top(adjustForTempInset: adjustForTempInset), animated: animated) } } @@ -1406,10 +1429,14 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout else { return } - self.selectedId = id - if let currentItemNode = self.currentItemNodeValue { - itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden) + + 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 = clippedScrollOffset + + let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false) } + + self.selectedId = id self.applyItemNodeAsCurrent(id: id, itemNode: itemNode) let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) 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) @@ -1423,7 +1450,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.filterEmptyAction(filter) }, secondaryEmptyAction: { [weak self] in self?.secondaryEmptyAction() - }) + }, autoSetReady: !animated) let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) @@ -1441,6 +1468,13 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } strongSelf.pendingItemNode = nil + itemNode.listNode.tempTopInset = strongSelf.tempTopInset + + if let controller = strongSelf.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { + let scrollOffset = clippedScrollOffset + + let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false) + } guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = strongSelf.validLayout else { strongSelf.itemNodes[id] = itemNode @@ -1506,6 +1540,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele completion?() })) + + if let (layout, _, visualNavigationHeight, originalNavigationHeight, _, insets, _, _, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { + itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate) + return + } } } } @@ -1541,7 +1580,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.filterEmptyAction(filter) }, secondaryEmptyAction: { [weak self] in self?.secondaryEmptyAction() - }) + }, autoSetReady: false) + itemNode.listNode.tempTopInset = self.tempTopInset self.itemNodes[id] = itemNode } } @@ -1643,6 +1683,10 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { var didBeginSelectingChatsWhileEditing: Bool = false var isEditing: Bool = false + private var tempAllowAvatarExpansion: Bool = false + private var tempDisableStoriesAnimations: Bool = false + private var tempNavigationScrollingTransition: ContainedViewLayoutTransition? + private var containerLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat)? var contentScrollingEnded: ((ListView) -> Bool)? @@ -1696,6 +1740,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.mainContainerNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded(listView: listView, isPrimary: true) ?? false } + self.mainContainerNode.endedInteractiveDragging = { [weak self] listView in + self?.endedInteractiveDragging(listView: listView, isPrimary: true) + } + self.mainContainerNode.shouldStopScrolling = { [weak self] listView, velocity in + return self?.shouldStopScrolling(listView: listView, velocity: velocity, isPrimary: true) ?? false + } self.addSubnode(self.debugListView) @@ -1735,6 +1785,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } if isLocked { self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + //self.controller?.requestLayout(transition: .immediate) } else { self.controller?.requestLayout(transition: .immediate) } @@ -1745,7 +1796,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return false } - if let storySubscriptions = controller.storySubscriptions, !storySubscriptions.items.isEmpty, !self.mainContainerNode.storiesUnlocked { + if let storySubscriptions = controller.storySubscriptions, !storySubscriptions.items.isEmpty { + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if navigationBarComponentView.storiesUnlocked { + return true + } + } return false } else { return true @@ -1880,7 +1936,6 @@ 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, @@ -1928,7 +1983,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } transition.updateFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) - return (navigationBarSize.height, navigationBarComponentView.effectiveStoriesInsetHeight) + return (navigationBarSize.height, 0.0) } else { return (0.0, 0.0) } @@ -1966,12 +2021,16 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } var allowAvatarsExpansion: Bool = true - if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.mainContainerNode.storiesUnlocked { - allowAvatarsExpansion = false + if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.tempAllowAvatarExpansion { + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if !navigationBarComponentView.storiesUnlocked { + allowAvatarsExpansion = false + } + } } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, transition: Transition(transition)) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: Transition(transition).withUserData(ChatListNavigationBar.AnimationHint(disableStoriesAnimations: self.tempDisableStoriesAnimations))) } } @@ -1982,16 +2041,26 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { let _ = self.updateNavigationBar(layout: layout, transition: transition) } - func scrollToStories() { + func scrollToStories(animated: Bool) { if self.inlineStackContainerNode != nil { return } - self.mainContainerNode.scrollToTop(animated: false) - self.mainContainerNode.storiesUnlocked = true + + if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + self.tempAllowAvatarExpansion = true + self.tempDisableStoriesAnimations = !animated + self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate + self.mainContainerNode.scrollToTop(animated: animated, adjustForTempInset: true) + self.tempAllowAvatarExpansion = false + self.tempDisableStoriesAnimations = false + tempNavigationScrollingTransition = nil + } + + /*self.mainContainerNode.scrollToTop(animated: false) self.mainContainerNode.ignoreStoryUnlockedScrolling = true self.controller?.requestLayout(transition: .immediate) self.mainContainerNode.scrollToTop(animated: false) - self.mainContainerNode.ignoreStoryUnlockedScrolling = false + self.mainContainerNode.ignoreStoryUnlockedScrolling = false*/ } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -2171,7 +2240,6 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } strongSelf.isSearchDisplayControllerActive = true - strongSelf.mainContainerNode.storiesUnlocked = false strongSelf.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate) strongSelf.searchDisplayController?.activate(insertSubnode: { [weak self] subnode, isSearchBar in @@ -2235,7 +2303,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { let _ = inlineStackContainerNode } - self.updateNavigationScrolling(transition: .immediate) + self.updateNavigationScrolling(transition: self.tempNavigationScrollingTransition ?? .immediate) /*if !isPrimary { self.contentOffsetChanged?(offset) @@ -2281,58 +2349,61 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { }*/ } - private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool { + private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool { 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 < ChatListNavigationBar.searchScrollHeight { - if searchScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { - let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true) - } else { - let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight + ChatListNavigationBar.searchScrollHeight, animated: true) - } - return true - } - } - } else { - if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight { - if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { - let _ = listView.scrollToOffsetFromTop(0.0, animated: true) - } else { - let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true) - } - return true - } - } - } + } else { + return false + } + + guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else { + return false + } + + if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { + let searchScrollOffset = clippedScrollOffset + if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight { + return true + } else if clippedScrollOffset < 0.0 && clippedScrollOffset > -listView.tempTopInset { + return true } } return false - - /*guard let inlineStackContainerNode = self.inlineStackContainerNode else { - return self.contentScrollingEnded?(listView) ?? false - } - - self.contentOffsetSyncLockedIn = false - - if isPrimary { + } + + private func endedInteractiveDragging(listView: ListView, isPrimary: Bool) { + } + + private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool { + if !isPrimary || self.inlineStackContainerNode == nil { + } else { return false } - let _ = inlineStackContainerNode + guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else { + return false + } - return self.contentScrollingEnded?(listView) ?? false*/ + if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { + let searchScrollOffset = clippedScrollOffset + if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight { + if searchScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { + let _ = listView.scrollToOffsetFromTop(0.0, animated: true) + } else { + let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true) + } + return true + } else if clippedScrollOffset < 0.0 && clippedScrollOffset > -listView.tempTopInset { + if navigationBarComponentView.storiesUnlocked { + let _ = listView.scrollToOffsetFromTop(-listView.tempTopInset, animated: true) + } else { + let _ = listView.scrollToOffsetFromTop(0.0, animated: true) + } + return true + } + } + + return false } func makeInlineChatList(location: ChatListControllerLocation) -> ChatListContainerNode { @@ -2348,7 +2419,11 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { func setInlineChatList(inlineStackContainerNode: ChatListContainerNode?) { if let inlineStackContainerNode = inlineStackContainerNode { if self.inlineStackContainerNode !== inlineStackContainerNode { - self.mainContainerNode.storiesUnlocked = false + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if navigationBarComponentView.storiesUnlocked { + let _ = self.mainContainerNode.currentItemNode.scrollToOffsetFromTop(self.mainContainerNode.currentItemNode.tempTopInset, animated: true) + } + } inlineStackContainerNode.leftSeparatorLayer.isHidden = false @@ -2367,6 +2442,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { inlineStackContainerNode.contentOffsetChanged = { [weak self] offset in self?.contentOffsetChanged(offset: offset, isPrimary: false) } + inlineStackContainerNode.endedInteractiveDragging = { [weak self] listView in + self?.endedInteractiveDragging(listView: listView, isPrimary: false) + } + inlineStackContainerNode.shouldStopScrolling = { [weak self] listView, velocity in + return self?.shouldStopScrolling(listView: listView, velocity: velocity, isPrimary: false) ?? false + } inlineStackContainerNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded(listView: listView, isPrimary: false) ?? false } @@ -2380,7 +2461,7 @@ 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) + let scrollOffset = max(0.0, clippedScrollOffset) inlineStackContainerNode.initialScrollingOffset = scrollOffset } @@ -2431,9 +2512,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { if let searchDisplayController = self.searchDisplayController { searchDisplayController.contentNode.scrollToTop() } else if let inlineStackContainerNode = self.inlineStackContainerNode { - inlineStackContainerNode.scrollToTop(animated: true) + inlineStackContainerNode.scrollToTop(animated: true, adjustForTempInset: false) } else { - self.mainContainerNode.scrollToTop(animated: true) + self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false) } } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fedf0f252d..233e64a135 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1040,7 +1040,7 @@ public enum ChatListGlobalScrollOption { } public enum ChatListNodeScrollPosition { - case top + case top(adjustForTempInset: Bool) } public enum ChatListNodeEmptyState: Equatable { @@ -1151,6 +1151,7 @@ public final class ChatListNode: ListView { public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? + public var didBeginInteractiveDragging: ((ListView) -> Void)? public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)? private var currentIsEmptyState: ChatListNodeEmptyState? @@ -1193,9 +1194,11 @@ public final class ChatListNode: ListView { } } - public private(set) var startedScrollingAtUpperBound: Bool = false + public var startedScrollingAtUpperBound: Bool = false - public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) { + private let autoSetReady: Bool + + public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool) { self.context = context self.location = location self.chatListFilter = chatListFilter @@ -1204,6 +1207,7 @@ public final class ChatListNode: ListView { self.mode = mode self.animationCache = animationCache self.animationRenderer = animationRenderer + self.autoSetReady = autoSetReady var isSelecting = false if case .peers(_, true, _, _, _, _) = mode { @@ -2363,7 +2367,7 @@ public final class ChatListNode: ListView { var isHiddenItemVisible = false if let range = range.visibleRange { let entryCount = chatListView.filteredEntries.count - for i in range.firstIndex ..< range.lastIndex { + for i in max(0, range.firstIndex - 1) ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue @@ -2384,11 +2388,11 @@ public final class ChatListNode: ListView { } } if !isHiddenItemVisible && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { - /*strongSelf.updateState { state in + strongSelf.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false return state - }*/ + } } } } @@ -2731,7 +2735,7 @@ public final class ChatListNode: ListView { case .none, .unknown: strongSelf.startedScrollingAtUpperBound = false case let .known(value): - strongSelf.startedScrollingAtUpperBound = value <= 0.0 + strongSelf.startedScrollingAtUpperBound = value <= 0.001 } if let canExpandHiddenItems = strongSelf.canExpandHiddenItems { @@ -2747,27 +2751,28 @@ public final class ChatListNode: ListView { return state } } + + strongSelf.didBeginInteractiveDragging?(strongSelf) } self.didEndScrolling = { [weak self] _ in guard let strongSelf = self else { return } - strongSelf.startedScrollingAtUpperBound = false let _ = strongSelf.contentScrollingEnded?(strongSelf) let revealHiddenItems: Bool switch strongSelf.visibleContentOffset() { case .none, .unknown: revealHiddenItems = false case let .known(value): - revealHiddenItems = value <= 54.0 + revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0 } if !revealHiddenItems && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { - strongSelf.updateState { state in + /*strongSelf.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false return state - } + }*/ } } @@ -2795,9 +2800,9 @@ public final class ChatListNode: ListView { case .none, .unknown: atTop = false case let .known(value): - atTop = value <= 0.0 + atTop = value <= -strongSelf.tempTopInset if strongSelf.startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking { - revealHiddenItems = value <= -60.0 + revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0 } } strongSelf.scrolledAtTopValue = atTop @@ -2939,7 +2944,7 @@ public final class ChatListNode: ListView { if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout { strongSelf.dequeueTransition() } else { - if !strongSelf.didSetReady { + if !strongSelf.didSetReady && strongSelf.autoSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } @@ -3282,6 +3287,7 @@ public final class ChatListNode: ListView { default: break } + additionalScrollDistance = 0.0 } else { additionalScrollDistance += previousStoriesInset - storiesInset } @@ -3311,15 +3317,24 @@ public final class ChatListNode: ListView { } public func scrollToPosition(_ position: ChatListNodeScrollPosition, animated: Bool = true) { + var additionalDelta: CGFloat = 0.0 + switch position { + case let .top(adjustForTempInset): + if adjustForTempInset { + additionalDelta = ChatListNavigationBar.storiesScrollHeight + self.tempTopInset = ChatListNavigationBar.storiesScrollHeight + } + } + if let list = self.chatListView?.originalList { if !list.hasLater { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(additionalDelta), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { - let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: animated, filter: self.chatListFilter) + let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } else { - let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: animated, filter: self.chatListFilter) + let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index c39e246b29..37d384f473 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -333,7 +333,11 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func contentScrollingEnded(listView: ListView) -> Bool { - if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if "".isEmpty { + return false + } + + /*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 { @@ -365,7 +369,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } } - } + }*/ return false } @@ -412,7 +416,6 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, isSearchActive: self.isSearchDisplayControllerActive, - storiesUnlocked: self.storiesUnlocked, primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, @@ -444,7 +447,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } transition.updateFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) - return (navigationBarSize.height, navigationBarComponentView.effectiveStoriesInsetHeight) + return (navigationBarSize.height, 0.0) } else { return (0.0, 0.0) } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 956260481b..471f020bcb 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -256,6 +256,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)? public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)? + public final var shouldStopScrolling: ((CGFloat) -> Bool)? public final var updateScrollingIndicator: ((ScrollingIndicatorState?, ContainedViewLayoutTransition) -> Void)? @@ -855,6 +856,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if let shouldStopScrolling = self.shouldStopScrolling, shouldStopScrolling(velocity.y) { + targetContentOffset.pointee.y = scrollView.contentOffset.y + } + } + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { self.isDragging = false if decelerate { @@ -871,10 +878,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.resetScrollIndicatorFlashTimer(start: true) self.lastContentOffsetTimestamp = 0.0 - self.didEndScrolling?(false) self.isAuxiliaryDisplayLinkEnabled = false } + self.ignoreScrollingEvents = true + self.ignoreScrollingEvents = false self.endedInteractiveDragging(self.touchesPosition) + if !decelerate { + self.didEndScrolling?(false) + } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { @@ -943,6 +954,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private var generalAccumulatedDeltaY: CGFloat = 0.0 private var previousDidScrollTimestamp: Double = 0.0 + private var ignoreNextScrollAdjustment: Bool = false + private func updateScrollViewDidScroll(_ scrollView: UIScrollView, synchronous: Bool) { if self.ignoreScrollingEvents || scroller !== self.scroller { return @@ -965,6 +978,16 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let deltaY = scrollView.contentOffset.y - self.lastContentOffset.y + /*if self.ignoreNextScrollAdjustment { + self.ignoreNextScrollAdjustment = false + self.lastContentOffset = scrollView.contentOffset + return + }*/ + + //if abs(deltaY) > 30.0 { + // print("deltaY: \(deltaY)") + //} + self.generalAccumulatedDeltaY += deltaY if abs(self.generalAccumulatedDeltaY) > 14.0 { let direction: GeneralScrollDirection = self.generalAccumulatedDeltaY < 0 ? .up : .down @@ -976,6 +999,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.lastContentOffset = scrollView.contentOffset + //print("lastContentOffset9 = \(self.lastContentOffset.y)") if !self.lastContentOffsetTimestamp.isZero { self.lastContentOffsetTimestamp = CACurrentMediaTime() } @@ -1141,6 +1165,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture return false } + public var tempTopInset: CGFloat = 0.0 { + didSet { + if self.tempTopInset != oldValue { + self.updateScroller(transition: .immediate) + } + } + } + private func snapToBounds(snapTopItem: Bool, stackFromBottom: Bool, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, scrollToItem: ListViewScrollToItem? = nil, isExperimentalSnapToScrollToItem: Bool = false, insetDeltaOffsetFix: CGFloat) -> (snappedTopInset: CGFloat, offset: CGFloat) { if self.itemNodes.count == 0 { return (0.0, 0.0) @@ -1175,7 +1207,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } if topItemFound { - topItemEdge = self.itemNodes[0].apparentFrame.origin.y + topItemEdge = self.itemNodes[0].apparentFrame.origin.y - self.tempTopInset } var bottomItemNode: ListViewItemNode? @@ -1643,31 +1675,39 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.ignoreScrollingEvents = true if topItemFound && bottomItemFound { self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight) - self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) + self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge + self.tempTopInset) + //print("lastContentOffset1 = \(self.lastContentOffset.y)") self.scroller.contentOffset = self.lastContentOffset } else if topItemFound { self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) - self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) + self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge + self.tempTopInset) + //print("lastContentOffset2 = \(self.lastContentOffset.y), ignoreNextScrollAdjustment: \(self.ignoreNextScrollAdjustment)") if self.scroller.contentOffset != self.lastContentOffset { self.scroller.contentOffset = self.lastContentOffset } + self.lastContentOffset = self.scroller.contentOffset + //print("lastContentOffset2.1 = \(self.lastContentOffset.y), ignoreNextScrollAdjustment: \(self.ignoreNextScrollAdjustment)") } else if bottomItemFound { self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge) + //print("lastContentOffset3 = \(self.lastContentOffset.y)") self.scroller.contentOffset = self.lastContentOffset } else if self.itemNodes.isEmpty { self.scroller.contentSize = self.visibleSize if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero { self.scroller.contentOffset = .zero self.lastContentOffset = .zero + //print("lastContentOffset4 = \(self.lastContentOffset.y)") } } else { self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 { self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + //print("lastContentOffset5 = \(self.lastContentOffset.y)") self.scroller.contentOffset = self.lastContentOffset } else { self.lastContentOffset = self.scroller.contentOffset + //print("lastContentOffset6 = \(self.lastContentOffset.y)") } } self.ignoreScrollingEvents = wasIgnoringScrollingEvents @@ -1805,9 +1845,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let wasIgnoringScrollingEvents = self.ignoreScrollingEvents self.ignoreScrollingEvents = true self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size) - self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0) - self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) - self.scroller.contentOffset = self.lastContentOffset + //self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0) + //self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + //print("lastContentOffset7 = \(self.lastContentOffset.y)") + //self.scroller.contentOffset = self.lastContentOffset self.ignoreScrollingEvents = wasIgnoringScrollingEvents self.updateScroller(transition: .immediate) @@ -2984,6 +3025,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture offsetFix += additionalScrollDistance + /*if let topItemNode = self.itemNodes.first(where: { $0.index == 0 }) { + let topEdge = self.scroller.contentOffset.y + updateSizeAndInsets.insets.top + offsetFix = -(topEdge - topItemNode.apparentFrame.minY) + }*/ + self.insets = updateSizeAndInsets.insets self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets self.scrollIndicatorInsets = updateSizeAndInsets.scrollIndicatorInsets ?? self.insets @@ -3137,10 +3183,17 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let wasIgnoringScrollingEvents = self.ignoreScrollingEvents self.ignoreScrollingEvents = true - self.scroller.frame = CGRect(origin: CGPoint(), size: self.visibleSize) - self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) - self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) - self.scroller.contentOffset = self.lastContentOffset + if self.scroller.bounds.size != self.visibleSize { + self.scroller.frame = CGRect(origin: CGPoint(), size: self.visibleSize) + } + //self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) + + //self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + //self.scroller.contentOffset = self.lastContentOffset + + //self.lastContentOffset = self.scroller.contentOffset + //print("lastContentOffset8 = \(self.lastContentOffset.y)") + self.ignoreScrollingEvents = wasIgnoringScrollingEvents } else { let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil && scrollToItem?.directionHint != .Down, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem, insetDeltaOffsetFix: 0.0) @@ -4878,10 +4931,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public func scrollToOffsetFromTop(_ offset: CGFloat, animated: Bool) -> Bool { for itemNode in self.itemNodes { if itemNode.index == 0 { - if animated { - self.scroller.setContentOffset(CGPoint(x: 0.0, y: offset), animated: animated) - } else { - self.scroller.contentOffset = CGPoint(x: 0.0, y: offset) + if self.scroller.contentOffset.y != offset + self.tempTopInset { + self.stopScrolling() + if animated { + self.scroller.setContentOffset(CGPoint(x: 0.0, y: offset + self.tempTopInset), animated: animated) + } else { + self.scroller.contentOffset = CGPoint(x: 0.0, y: offset + self.tempTopInset) + } } return true } diff --git a/submodules/Display/Source/ListViewScroller.swift b/submodules/Display/Source/ListViewScroller.swift index 143444d07f..67f5b22e65 100644 --- a/submodules/Display/Source/ListViewScroller.swift +++ b/submodules/Display/Source/ListViewScroller.swift @@ -5,9 +5,7 @@ public final class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { super.init(frame: frame) self.scrollsToTop = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.contentInsetAdjustmentBehavior = .never - } + self.contentInsetAdjustmentBehavior = .never } required public init?(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 18f5afa686..3b1bb6298d 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -136,7 +136,7 @@ public final class ChatListHeaderComponent: Component { public let storySubscriptions: EngineStorySubscriptions? public let storiesIncludeHidden: Bool public let storiesFraction: CGFloat - public let storiesUnlockedFraction: CGFloat + public let storiesUnlocked: Bool public let uploadProgress: Float? public let context: AccountContext public let theme: PresentationTheme @@ -154,7 +154,7 @@ public final class ChatListHeaderComponent: Component { storySubscriptions: EngineStorySubscriptions?, storiesIncludeHidden: Bool, storiesFraction: CGFloat, - storiesUnlockedFraction: CGFloat, + storiesUnlocked: Bool, uploadProgress: Float?, context: AccountContext, theme: PresentationTheme, @@ -171,7 +171,7 @@ public final class ChatListHeaderComponent: Component { self.storySubscriptions = storySubscriptions self.storiesIncludeHidden = storiesIncludeHidden self.storiesFraction = storiesFraction - self.storiesUnlockedFraction = storiesUnlockedFraction + self.storiesUnlocked = storiesUnlocked self.uploadProgress = uploadProgress self.theme = theme self.strings = strings @@ -204,7 +204,7 @@ public final class ChatListHeaderComponent: Component { if lhs.storiesFraction != rhs.storiesFraction { return false } - if lhs.storiesUnlockedFraction != rhs.storiesUnlockedFraction { + if lhs.storiesUnlocked != rhs.storiesUnlocked { return false } if lhs.uploadProgress != rhs.uploadProgress { @@ -312,6 +312,8 @@ public final class ChatListHeaderComponent: Component { var contentOffsetFraction: CGFloat = 0.0 private(set) var centerContentWidth: CGFloat = 0.0 + private(set) var centerContentRightInset: CGFloat = 0.0 + private(set) var centerContentOffsetX: CGFloat = 0.0 private(set) var centerContentOrigin: CGFloat = 0.0 @@ -637,6 +639,9 @@ public final class ChatListHeaderComponent: Component { } } + var centerContentRightInset: CGFloat = 0.0 + centerContentRightInset = size.width - rightOffset + 16.0 + var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 var centerContentOrigin: CGFloat = 0.0 @@ -699,6 +704,7 @@ public final class ChatListHeaderComponent: Component { self.centerContentWidth = centerContentWidth self.centerContentOffsetX = centerContentOffsetX self.centerContentOrigin = centerContentOrigin + self.centerContentRightInset = centerContentRightInset } } @@ -809,49 +815,6 @@ public final class ChatListHeaderComponent: Component { let previousComponent = self.component self.component = component - var storyListTransition = transition - - if let storySubscriptions = component.storySubscriptions { - let storyPeerList: ComponentView - if let current = self.storyPeerList { - storyPeerList = current - } else { - storyListTransition = .immediate - storyPeerList = ComponentView() - self.storyPeerList = storyPeerList - } - - let _ = storyPeerList.update( - transition: storyListTransition, - component: AnyComponent(StoryPeerListComponent( - externalState: self.storyPeerListExternalState, - context: component.context, - theme: component.theme, - strings: component.strings, - sideInset: component.sideInset, - useHiddenList: component.storiesIncludeHidden, - storySubscriptions: storySubscriptions, - collapseFraction: 1.0 - component.storiesFraction, - unlockedFraction: 1.0 - component.storiesUnlockedFraction, - uploadProgress: component.uploadProgress, - peerAction: { [weak self] peer in - guard let self else { - return - } - self.storyPeerAction?(peer) - }, - contextPeerAction: { [weak self] sourceNode, gesture, peer in - guard let self else { - return - } - self.storyContextPeerAction?(sourceNode, gesture, peer) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 94.0) - ) - } - if let primaryContent = component.primaryContent { var primaryContentTransition = transition let primaryContentView: ContentView @@ -883,15 +846,15 @@ public final class ChatListHeaderComponent: Component { self.addSubview(primaryContentView) } - var sideContentWidth: CGFloat = 0.0 - if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { + let sideContentWidth: CGFloat = 0.0 + /*if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { sideContentWidth = self.storyPeerListExternalState.collapsedWidth + 12.0 } if let chatListTitle = primaryContent.chatListTitle { if chatListTitle.activity { sideContentWidth = 0.0 } - } + }*/ primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -902,6 +865,59 @@ public final class ChatListHeaderComponent: Component { primaryContentView.removeFromSuperview() } + var storyListTransition = transition + if let storySubscriptions = component.storySubscriptions { + let storyPeerList: ComponentView + if let current = self.storyPeerList { + storyPeerList = current + } else { + storyListTransition = .immediate + storyPeerList = ComponentView() + self.storyPeerList = storyPeerList + } + + let _ = storyPeerList.update( + transition: storyListTransition, + component: AnyComponent(StoryPeerListComponent( + externalState: self.storyPeerListExternalState, + context: component.context, + theme: component.theme, + strings: component.strings, + sideInset: component.sideInset, + titleContentWidth: self.primaryContentView?.centerContentWidth ?? 0.0, + maxTitleX: availableSize.width - (self.primaryContentView?.centerContentRightInset ?? 0.0), + useHiddenList: component.storiesIncludeHidden, + storySubscriptions: storySubscriptions, + collapseFraction: 1.0 - component.storiesFraction, + unlocked: component.storiesUnlocked, + uploadProgress: component.uploadProgress, + peerAction: { [weak self] peer in + guard let self else { + return + } + self.storyPeerAction?(peer) + }, + contextPeerAction: { [weak self] sourceNode, gesture, peer in + guard let self else { + return + } + self.storyContextPeerAction?(sourceNode, gesture, peer) + }, + updateTitleContentOffset: { [weak self] offset, transition in + guard let self, let primaryContentView = self.primaryContentView else { + return + } + guard let chatListTitleView = primaryContentView.chatListTitleView else { + return + } + transition.setSublayerTransform(view: chatListTitleView, transform: CATransform3DMakeTranslation(offset, 0.0, 0.0)) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 94.0) + ) + } + if let secondaryContent = component.secondaryContent { var secondaryContentTransition = transition let secondaryContentView: ContentView @@ -981,17 +997,18 @@ public final class ChatListHeaderComponent: Component { self.addSubview(storyPeerListComponentView) } - let storyPeerListMinOffset: CGFloat = -7.0 + //let storyPeerListMinOffset: CGFloat = -7.0 let storyPeerListMaxOffset: CGFloat = availableSize.height + 8.0 - let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction + //let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction var defaultStoryListX: CGFloat = 0.0 if let primaryContentView = self.primaryContentView { defaultStoryListX = primaryContentView.centerContentOrigin - (self.storyPeerListExternalState.collapsedWidth * 0.5 + 12.0) - availableSize.width * 0.5 } + let _ = defaultStoryListX - storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 79.0))) + storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + 0.0, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0))) var storyListNormalAlpha: CGFloat = 1.0 if let chatListTitle = component.primaryContent?.chatListTitle { diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index d502f07dcd..5019cf5fc3 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -8,15 +8,23 @@ import ComponentDisplayAdapters import SearchUI import AccountContext import TelegramCore +import StoryPeerListComponent public final class ChatListNavigationBar: Component { + public final class AnimationHint { + let disableStoriesAnimations: Bool + + public init(disableStoriesAnimations: Bool) { + self.disableStoriesAnimations = disableStoriesAnimations + } + } + public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings 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 @@ -37,7 +45,6 @@ public final class ChatListNavigationBar: Component { statusBarHeight: CGFloat, sideInset: CGFloat, isSearchActive: Bool, - storiesUnlocked: Bool, primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, @@ -57,7 +64,6 @@ 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 @@ -91,9 +97,6 @@ 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 } @@ -160,15 +163,7 @@ public final class ChatListNavigationBar: Component { 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 storiesOffsetStartFraction: CGFloat = 1.0 - private var applyScrollUnlockedFraction: CGFloat = 1.0 - private var storiesOffsetFraction: CGFloat = 0.0 - private var storiesUnlockedFraction: CGFloat = 0.0 - private var storiesUnlockedStartFraction: CGFloat = 1.0 + public private(set) var storiesUnlocked: Bool = false private var tabsNode: ASDisplayNode? private var tabsNodeIsSearch: Bool = false @@ -198,9 +193,16 @@ public final class ChatListNavigationBar: Component { return nil } - guard let result = super.hitTest(point, with: event) else { + if self.alpha.isZero { return nil } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) return result } @@ -211,13 +213,10 @@ public final class ChatListNavigationBar: Component { } public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) { - if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion, !transition.animation.isImmediate { - self.addStoriesUnlockedAnimation(duration: 0.3, animateScrollUnlocked: false) - } - let transition = transition self.rawScrollOffset = offset + let allowAvatarsExpansionUpdated = self.currentAllowAvatarsExpansion != allowAvatarsExpansion self.currentAllowAvatarsExpansion = allowAvatarsExpansion if self.deferScrollApplication && !forceUpdate { @@ -235,19 +234,11 @@ public final class ChatListNavigationBar: Component { self.scrollStrings = component.strings let searchOffsetDistance: CGFloat = ChatListNavigationBar.searchScrollHeight - let defaultStoriesOffsetDistance: CGFloat = ChatListNavigationBar.storiesScrollHeight - let effectiveStoriesOffsetDistance: CGFloat - var minContentOffset: CGFloat = ChatListNavigationBar.searchScrollHeight - if !component.isSearchActive, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, component.storiesUnlocked { - effectiveStoriesOffsetDistance = defaultStoriesOffsetDistance * (1.0 - component.secondaryTransition) - minContentOffset += effectiveStoriesOffsetDistance - } else { - effectiveStoriesOffsetDistance = 0.0 - } + let minContentOffset: CGFloat = ChatListNavigationBar.searchScrollHeight let clippedScrollOffset = min(minContentOffset, offset) - if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate { + if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate && !allowAvatarsExpansionUpdated { return } self.hasDeferredScrollOffset = false @@ -302,7 +293,7 @@ public final class ChatListNavigationBar: Component { self.addSubview(searchContentNode.view) } - let clippedStoriesOverscrollOffset = -min(0.0, clippedScrollOffset) + /*let clippedStoriesOverscrollOffset = -min(0.0, clippedScrollOffset) let clippedStoriesOffset = max(0.0, min(clippedScrollOffset, defaultStoriesOffsetDistance)) var storiesOffsetFraction: CGFloat var storiesUnlockedOffsetFraction: CGFloat @@ -322,7 +313,7 @@ public final class ChatListNavigationBar: Component { if self.applyScrollFractionAnimator != nil { storiesOffsetFraction = self.applyScrollFraction * storiesOffsetFraction + (1.0 - self.applyScrollFraction) * self.storiesOffsetStartFraction storiesUnlockedOffsetFraction = self.applyScrollUnlockedFraction * storiesUnlockedOffsetFraction + (1.0 - self.applyScrollUnlockedFraction) * self.storiesUnlockedStartFraction - } + }*/ let searchSize = CGSize(width: currentLayout.size.width, height: navigationBarSearchContentHeight) var searchFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - searchSize.height), size: searchSize) @@ -333,7 +324,7 @@ public final class ChatListNavigationBar: Component { searchFrame.origin.y -= component.accessoryPanelContainerHeight } - let clippedSearchOffset = max(0.0, min(clippedScrollOffset - effectiveStoriesOffsetDistance, searchOffsetDistance)) + let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance searchContentNode.expansionProgress = 1.0 - searchOffsetFraction @@ -341,13 +332,38 @@ public final class ChatListNavigationBar: Component { searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) - var headerTransition = transition - if self.applyScrollFractionAnimator != nil { + let headerTransition = transition + /*if self.applyScrollFractionAnimator != nil { headerTransition = .immediate + }*/ + + let storiesOffsetFraction: CGFloat + let storiesUnlocked: Bool + if allowAvatarsExpansion { + storiesOffsetFraction = max(0.0, min(1.0, -offset / ChatListNavigationBar.storiesScrollHeight)) + + if offset <= -80.0 { + storiesUnlocked = true + } else if offset >= -66.0 { + storiesUnlocked = false + } else { + storiesUnlocked = self.storiesUnlocked + } + } else { + storiesOffsetFraction = 0.0 + storiesUnlocked = false } - self.storiesOffsetFraction = storiesOffsetFraction - self.storiesUnlockedFraction = storiesUnlockedOffsetFraction + if allowAvatarsExpansion && transition.animation.isImmediate { + if self.storiesUnlocked != storiesUnlocked { + if storiesUnlocked { + HapticFeedback().impact() + } else { + HapticFeedback().tap() + } + } + } + self.storiesUnlocked = storiesUnlocked let headerComponent = ChatListHeaderComponent( sideInset: component.sideInset + 16.0, @@ -357,8 +373,8 @@ public final class ChatListNavigationBar: Component { networkStatus: nil, storySubscriptions: component.storySubscriptions, storiesIncludeHidden: component.storiesIncludeHidden, - storiesFraction: 1.0 - storiesOffsetFraction, - storiesUnlockedFraction: 1.0 - storiesUnlockedOffsetFraction, + storiesFraction: storiesOffsetFraction, + storiesUnlocked: storiesUnlocked, uploadProgress: component.uploadProgress, context: component.context, theme: component.theme, @@ -376,16 +392,29 @@ public final class ChatListNavigationBar: Component { component.context.sharedContext.appLockContext.lock() } ) + + let animationHint = transition.userData(AnimationHint.self) + + var animationDuration: Double? + if case let .curve(duration, _) = transition.animation { + animationDuration = duration + } + self.currentHeaderComponent = headerComponent let headerContentSize = self.headerContent.update( - transition: headerTransition, + transition: headerTransition.withUserData(StoryPeerListComponent.AnimationHint( + duration: animationDuration, + allowAvatarsExpansionUpdated: allowAvatarsExpansionUpdated && allowAvatarsExpansion, + bounce: transition.animation.isImmediate, + disableAnimations: animationHint?.disableStoriesAnimations ?? false + )), component: AnyComponent(headerComponent), environment: {}, containerSize: CGSize(width: currentLayout.size.width, height: 44.0) ) let headerContentY: CGFloat if component.isSearchActive { - headerContentY = -headerContentSize.height - effectiveStoriesOffsetDistance + headerContentY = -headerContentSize.height } else { if component.statusBarHeight < 1.0 { headerContentY = 0.0 @@ -496,7 +525,6 @@ public final class ChatListNavigationBar: Component { statusBarHeight: component.statusBarHeight, sideInset: component.sideInset, isSearchActive: component.isSearchActive, - storiesUnlocked: component.storiesUnlocked, primaryContent: component.primaryContent, secondaryContent: component.secondaryContent, secondaryTransition: component.secondaryTransition, @@ -520,7 +548,7 @@ public final class ChatListNavigationBar: Component { storySubscriptions: headerComponent.storySubscriptions, storiesIncludeHidden: headerComponent.storiesIncludeHidden, storiesFraction: headerComponent.storiesFraction, - storiesUnlockedFraction: headerComponent.storiesUnlockedFraction, + storiesUnlocked: headerComponent.storiesUnlocked, uploadProgress: storyUploadProgress, context: headerComponent.context, theme: headerComponent.theme, @@ -543,12 +571,8 @@ public final class ChatListNavigationBar: Component { func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme - var storiesUnlockedUpdated = false var uploadProgressUpdated = false if let previousComponent = self.component { - if previousComponent.storiesUnlocked != component.storiesUnlocked { - storiesUnlockedUpdated = true - } if previousComponent.uploadProgress != component.uploadProgress { uploadProgressUpdated = true } @@ -573,16 +597,7 @@ public final class ChatListNavigationBar: Component { if component.statusBarHeight < 1.0 { contentHeight += 8.0 } - self.effectiveStoriesInsetHeight = 0.0 } else { - if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, component.storiesUnlocked { - let storiesHeight: CGFloat = ChatListNavigationBar.storiesScrollHeight * (1.0 - component.secondaryTransition) - contentHeight += storiesHeight - self.effectiveStoriesInsetHeight = storiesHeight - } else { - self.effectiveStoriesInsetHeight = 0.0 - } - contentHeight += navigationBarSearchContentHeight } @@ -605,14 +620,10 @@ public final class ChatListNavigationBar: Component { } } - if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation { - self.addStoriesUnlockedAnimation(duration: duration, animateScrollUnlocked: true) - } - return size } - private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) { + /*private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) { guard let component = self.component else { return } @@ -648,7 +659,7 @@ public final class ChatListNavigationBar: Component { self.applyScrollFractionAnimator?.invalidate() self.applyScrollFractionAnimator = nil }) - } + }*/ } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 70cbcec0ad..59e34ca326 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -11,6 +11,32 @@ import SwiftSignalKit import TelegramPresentationData import StoryContainerScreen +private func solveParabolicMotion(from sourcePoint: CGPoint, to targetPosition: CGPoint, progress: CGFloat) -> CGPoint { + if sourcePoint.y == targetPosition.y { + return sourcePoint.interpolate(to: targetPosition, amount: progress) + } + + //(x - h)² + (y - k)² = r² + //(x1 - h) * (x1 - h) + (y1 - k) * (y1 - k) = r * r + //(x2 - h) * (x2 - h) + (y2 - k) * (y2 - k) = r * r + + let x1 = sourcePoint.y + let y1 = sourcePoint.x + let x2 = targetPosition.y + let y2 = targetPosition.x + + let b = (x1 * x1 * y2 - x2 * x2 * y1) / (x1 * x1 - x2 * x2) + let k = (y1 - y2) / (x1 * x1 - x2 * x2) + + let x = sourcePoint.y.interpolate(to: targetPosition.y, amount: progress) + let y = k * x * x + b + return CGPoint(x: y, y: x) +} + +private let modelSpringAnimation: CABasicAnimation = { + return makeSpringBounceAnimation("", 0.0, 88.0) +}() + public final class StoryPeerListComponent: Component { public final class ExternalState { public fileprivate(set) var collapsedWidth: CGFloat = 0.0 @@ -19,18 +45,35 @@ public final class StoryPeerListComponent: Component { } } + public final class AnimationHint { + let duration: Double? + let allowAvatarsExpansionUpdated: Bool + let bounce: Bool + let disableAnimations: Bool + + public init(duration: Double?, allowAvatarsExpansionUpdated: Bool, bounce: Bool, disableAnimations: Bool) { + self.duration = duration + self.allowAvatarsExpansionUpdated = allowAvatarsExpansionUpdated + self.bounce = bounce + self.disableAnimations = disableAnimations + } + } + public let externalState: ExternalState public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let sideInset: CGFloat + public let titleContentWidth: CGFloat + public let maxTitleX: CGFloat public let useHiddenList: Bool public let storySubscriptions: EngineStorySubscriptions? public let collapseFraction: CGFloat - public let unlockedFraction: CGFloat + public let unlocked: Bool public let uploadProgress: Float? public let peerAction: (EnginePeer?) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void + public let updateTitleContentOffset: (CGFloat, Transition) -> Void public init( externalState: ExternalState, @@ -38,26 +81,32 @@ public final class StoryPeerListComponent: Component { theme: PresentationTheme, strings: PresentationStrings, sideInset: CGFloat, + titleContentWidth: CGFloat, + maxTitleX: CGFloat, useHiddenList: Bool, storySubscriptions: EngineStorySubscriptions?, collapseFraction: CGFloat, - unlockedFraction: CGFloat, + unlocked: Bool, uploadProgress: Float?, peerAction: @escaping (EnginePeer?) -> Void, - contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void + contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, + updateTitleContentOffset: @escaping (CGFloat, Transition) -> Void ) { self.externalState = externalState self.context = context self.theme = theme self.strings = strings self.sideInset = sideInset + self.titleContentWidth = titleContentWidth + self.maxTitleX = maxTitleX self.useHiddenList = useHiddenList self.storySubscriptions = storySubscriptions self.collapseFraction = collapseFraction - self.unlockedFraction = unlockedFraction + self.unlocked = unlocked self.uploadProgress = uploadProgress self.peerAction = peerAction self.contextPeerAction = contextPeerAction + self.updateTitleContentOffset = updateTitleContentOffset } public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { @@ -73,6 +122,12 @@ public final class StoryPeerListComponent: Component { if lhs.sideInset != rhs.sideInset { return false } + if lhs.titleContentWidth != rhs.titleContentWidth { + return false + } + if lhs.maxTitleX != rhs.maxTitleX { + return false + } if lhs.useHiddenList != rhs.useHiddenList { return false } @@ -82,7 +137,7 @@ public final class StoryPeerListComponent: Component { if lhs.collapseFraction != rhs.collapseFraction { return false } - if lhs.unlockedFraction != rhs.unlockedFraction { + if lhs.unlocked != rhs.unlocked { return false } if lhs.uploadProgress != rhs.uploadProgress { @@ -134,6 +189,44 @@ public final class StoryPeerListComponent: Component { } } + private final class AnimationState { + let duration: Double + let fromIsUnlocked: Bool + let fromFraction: CGFloat + let startTime: Double + let bounce: Bool + + init( + duration: Double, + fromIsUnlocked: Bool, + fromFraction: CGFloat, + startTime: Double, + bounce: Bool + ) { + self.duration = duration + self.fromIsUnlocked = fromIsUnlocked + self.fromFraction = fromFraction + self.startTime = startTime + self.bounce = bounce + } + + func interpolatedFraction(at timestamp: Double, effectiveFromFraction: CGFloat, toFraction: CGFloat) -> CGFloat { + var rawProgress = CGFloat((timestamp - self.startTime) / self.duration) + rawProgress = max(0.0, min(1.0, rawProgress)) + let progress = listViewAnimationCurveSystem(rawProgress) + + return effectiveFromFraction * (1.0 - progress) + toFraction * progress + } + + func isFinished(at timestamp: Double) -> Bool { + if timestamp > self.startTime + self.duration { + return true + } else { + return false + } + } + } + public final class View: UIView, UIScrollViewDelegate { private let collapsedButton: HighlightableButton private let scrollView: ScrollView @@ -153,6 +246,11 @@ public final class StoryPeerListComponent: Component { private var previewedItemDisposable: Disposable? private var previewedItemId: EnginePeer.Id? + private var animationState: AnimationState? + private var animator: ConstantDisplayLinkAnimator? + + private var currentFraction: CGFloat = 0.0 + public override init(frame: CGRect) { self.collapsedButton = HighlightableButton() @@ -165,6 +263,7 @@ public final class StoryPeerListComponent: Component { self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceVertical = false self.scrollView.alwaysBounceHorizontal = true + self.scrollView.clipsToBounds = false super.init(frame: frame) @@ -281,19 +380,198 @@ public final class StoryPeerListComponent: Component { let collapseEndIndex = collapseStartIndex + max(0, Int(collapsedItemCount) - 1) - let collapsedContentOrigin: CGFloat + var collapsedContentOrigin: CGFloat let collapsedItemOffsetY: CGFloat - let itemScale: CGFloat - collapsedContentOrigin = floor((itemLayout.containerSize.width - collapsedContentWidth) * 0.5) - itemScale = 1.0 - collapsedItemOffsetY = 0.0 + let titleContentSpacing: CGFloat = 8.0 + var combinedTitleContentWidth = component.titleContentWidth + if !combinedTitleContentWidth.isZero { + combinedTitleContentWidth += titleContentSpacing + } + let centralContentWidth: CGFloat = collapsedContentWidth + combinedTitleContentWidth + collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5) + collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0) + collapsedItemOffsetY = -59.0 + + struct CollapseState { + var globalFraction: CGFloat + var scaleFraction: CGFloat + var minFraction: CGFloat + var maxFraction: CGFloat + var sideAlphaFraction: CGFloat + } + + /*let calculateCollapedFraction: (CGFloat) -> CGFloat = { t in + let offset = scrollingRubberBandingOffset(offset: (1.0 - t) * 94.0, bandingStart: 0.0, range: 400.0, coefficient: 0.4) + return 1.0 - max(0.0, min(1.0, offset / 94.0)) + }*/ + + let targetExpandedFraction = component.collapseFraction + + let targetFraction: CGFloat = component.collapseFraction + + let targetScaleFraction: CGFloat + let targetMinFraction: CGFloat + let targetMaxFraction: CGFloat + let targetSideAlphaFraction: CGFloat + + if component.unlocked { + targetScaleFraction = targetExpandedFraction + targetMinFraction = 0.0 + targetMaxFraction = 1.0 - targetExpandedFraction + targetSideAlphaFraction = 1.0 + } else { + targetScaleFraction = 1.0 + targetMinFraction = 1.0 - targetExpandedFraction + targetMaxFraction = 0.0 + targetSideAlphaFraction = 0.0 + } + + let collapsedState: CollapseState + let expandBoundsFraction: CGFloat + if let animationState = self.animationState { + let effectiveFromScaleFraction: CGFloat + if animationState.fromIsUnlocked { + effectiveFromScaleFraction = animationState.fromFraction + } else { + effectiveFromScaleFraction = 1.0 + } + + let effectiveFromMinFraction: CGFloat + let effectiveFromMaxFraction: CGFloat + if animationState.fromIsUnlocked { + effectiveFromMinFraction = 0.0 + effectiveFromMaxFraction = 1.0 - animationState.fromFraction + } else { + effectiveFromMinFraction = 1.0 - animationState.fromFraction + effectiveFromMaxFraction = 0.0 + } + + let effectiveFromSideAlphaFraction: CGFloat + if animationState.fromIsUnlocked { + effectiveFromSideAlphaFraction = 1.0 + } else { + effectiveFromSideAlphaFraction = 0.0 + } + + let timestamp = CACurrentMediaTime() + + let animatedGlobalFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromFraction, toFraction: targetFraction) + let animatedScaleFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromScaleFraction, toFraction: targetScaleFraction) + let animatedMinFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMinFraction, toFraction: targetMinFraction) + let animatedMaxFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMaxFraction, toFraction: targetMaxFraction) + let animatedSideAlphaFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromSideAlphaFraction, toFraction: targetSideAlphaFraction) + + collapsedState = CollapseState( + globalFraction: animatedGlobalFraction, + scaleFraction: animatedScaleFraction, + minFraction: animatedMinFraction, + maxFraction: animatedMaxFraction, + sideAlphaFraction: animatedSideAlphaFraction + ) + + var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) + rawProgress = max(0.0, min(1.0, rawProgress)) + + if !animationState.fromIsUnlocked && animationState.bounce { + expandBoundsFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: 1.0, toFraction: 0.0) + } else { + expandBoundsFraction = 0.0 + } + } else { + collapsedState = CollapseState( + globalFraction: component.collapseFraction, + scaleFraction: targetScaleFraction, + minFraction: targetMinFraction, + maxFraction: targetMaxFraction, + sideAlphaFraction: targetSideAlphaFraction + ) + expandBoundsFraction = 0.0 + } + + let defaultCollapsedTitleOffset = floor((itemLayout.containerSize.width - component.titleContentWidth) * 0.5) + let targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentWidth + titleContentSpacing + let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset + + let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction) + let titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: 0.0 as CGFloat, amount: collapsedState.maxFraction) + + component.updateTitleContentOffset(titleContentOffset, transition) + + self.currentFraction = collapsedState.globalFraction component.externalState.collapsedWidth = collapsedContentWidth let effectiveVisibleBounds = self.scrollView.bounds let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0) + struct MeasuredItem { + var itemFrame: CGRect + var itemScale: CGFloat + } + let calculateItem: (Int) -> MeasuredItem = { i in + let regularItemFrame = itemLayout.frame(at: i) + let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame) + + let collapsedItemX: CGFloat + if i < collapseStartIndex { + collapsedItemX = collapsedContentOrigin + } else if i > collapseEndIndex { + collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5 + } else { + collapsedItemX = collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance + } + let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height)) + + var collapsedMaxItemFrame = collapsedItemFrame + + var collapseDistance: CGFloat = CGFloat(i - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex) + collapseDistance = max(0.0, min(1.0, collapseDistance)) + collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0 + collapsedMaxItemFrame.origin.x += collapseDistance * 20.0 + collapsedMaxItemFrame.origin.y += collapseDistance * 20.0 + collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0 + + let minimizedItemScale: CGFloat = 24.0 / 52.0 + let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0 + + let maximizedItemScale: CGFloat = 1.0 + + let minItemScale = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) + let itemScale: CGFloat = minItemScale.interpolate(to: maximizedItemScale, amount: collapsedState.maxFraction) + + let itemFrame: CGRect + if isReallyVisible { + var adjustedRegularFrame = regularItemFrame + if i < collapseStartIndex { + adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseStartIndex), amount: 0.0) + } else if i > collapseEndIndex { + adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseEndIndex), amount: 0.0) + } + + let collapsedItemPosition: CGPoint = collapsedItemFrame.center.interpolate(to: collapsedMaxItemFrame.center, amount: collapsedState.minFraction) + + var itemPosition = collapsedItemPosition.interpolate(to: adjustedRegularFrame.center, amount: collapsedState.maxFraction) + + var bounceOffsetFraction = (adjustedRegularFrame.midX - itemLayout.frame(at: collapseStartIndex).midX) / itemLayout.containerSize.width + bounceOffsetFraction = max(-1.0, min(1.0, bounceOffsetFraction)) + itemPosition.x += min(10.0, expandBoundsFraction * collapsedState.maxFraction * 1200.0) * bounceOffsetFraction + + //let itemPosition = solveParabolicMotion(from: collapsedItemPosition, to: adjustedRegularFrame.center, progress: collapsedState.maxFraction) + + let itemSize = CGSize(width: adjustedRegularFrame.width * itemScale, height: adjustedRegularFrame.height) + + itemFrame = itemSize.centered(around: itemPosition) + } else { + itemFrame = regularItemFrame + } + + return MeasuredItem( + itemFrame: itemFrame, + itemScale: itemScale + ) + } + var validIds: [EnginePeer.Id] = [] for i in 0 ..< self.sortedItems.count { let itemSet = self.sortedItems[i] @@ -334,33 +612,7 @@ public final class StoryPeerListComponent: Component { } } - let collapsedItemX: CGFloat - let collapsedItemScaleFactor: CGFloat - if i < collapseStartIndex { - collapsedItemX = collapsedContentOrigin - collapsedItemScaleFactor = 0.1 - } else if i > collapseEndIndex { - collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5 - collapsedItemScaleFactor = 0.1 - } else { - collapsedItemX = collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance - collapsedItemScaleFactor = 1.0 - } - let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height)) - - let itemFrame: CGRect - if isReallyVisible { - var adjustedRegularFrame = regularItemFrame - if i < collapseStartIndex { - adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseStartIndex), amount: 1.0 - component.unlockedFraction) - } else if i > collapseEndIndex { - adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseEndIndex), amount: 1.0 - component.unlockedFraction) - } - - itemFrame = adjustedRegularFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction) - } else { - itemFrame = regularItemFrame - } + let measuredItem = calculateItem(i) var leftItemFrame: CGRect? var rightItemFrame: CGRect? @@ -372,31 +624,23 @@ public final class StoryPeerListComponent: Component { isCollapsable = true if i != collapseStartIndex { - let regularLeftItemFrame = itemLayout.frame(at: i - 1) - let collapsedLeftItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularLeftItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularLeftItemFrame.height)) - leftItemFrame = regularLeftItemFrame.interpolate(to: collapsedLeftItemFrame, amount: component.collapseFraction) + leftItemFrame = calculateItem(i - 1).itemFrame } if i != collapseEndIndex { - let regularRightItemFrame = itemLayout.frame(at: i - 1) - let collapsedRightItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularRightItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularRightItemFrame.height)) - rightItemFrame = regularRightItemFrame.interpolate(to: collapsedRightItemFrame, amount: component.collapseFraction) + rightItemFrame = calculateItem(i + 1).itemFrame } } else { - if component.collapseFraction == 1.0 || component.unlockedFraction == 0.0 { - itemAlpha = 0.0 - } else { - itemAlpha = 1.0 - } + itemAlpha = collapsedState.sideAlphaFraction } - var leftNeighborDistance: CGFloat? - var rightNeighborDistance: CGFloat? + var leftNeighborDistance: CGPoint? + var rightNeighborDistance: CGPoint? if let leftItemFrame { - leftNeighborDistance = abs(leftItemFrame.midX - itemFrame.midX) + leftNeighborDistance = CGPoint(x: abs(leftItemFrame.midX - measuredItem.itemFrame.midX), y: leftItemFrame.minY - measuredItem.itemFrame.minY) } if let rightItemFrame { - rightNeighborDistance = abs(rightItemFrame.midX - itemFrame.midX) + rightNeighborDistance = CGPoint(x: abs(rightItemFrame.midX - measuredItem.itemFrame.midX), y: rightItemFrame.minY - measuredItem.itemFrame.minY) } let _ = visibleItem.view.update( @@ -409,9 +653,10 @@ public final class StoryPeerListComponent: Component { hasUnseen: hasUnseen, hasItems: hasItems, ringAnimation: itemRingAnimation, - collapseFraction: isReallyVisible ? component.collapseFraction : 0.0, - collapsedScaleFactor: collapsedItemScaleFactor, + collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0, + scale: measuredItem.itemScale, collapsedWidth: collapsedItemWidth, + expandedAlphaFraction: collapsedState.sideAlphaFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, action: component.peerAction, @@ -435,13 +680,13 @@ public final class StoryPeerListComponent: Component { itemView.backgroundContainer.layer.zPosition = 0.0 } - itemTransition.setFrame(view: itemView, frame: itemFrame) + itemTransition.setFrame(view: itemView, frame: measuredItem.itemFrame) itemTransition.setAlpha(view: itemView, alpha: itemAlpha) - itemTransition.setScale(view: itemView, scale: itemScale) + itemTransition.setScale(view: itemView, scale: 1.0) - itemTransition.setFrame(view: itemView.backgroundContainer, frame: itemFrame) + itemTransition.setFrame(view: itemView.backgroundContainer, frame: measuredItem.itemFrame) itemTransition.setAlpha(view: itemView.backgroundContainer, alpha: itemAlpha) - itemTransition.setScale(view: itemView.backgroundContainer, scale: itemScale) + itemTransition.setScale(view: itemView.backgroundContainer, scale: 1.0) itemView.updateIsPreviewing(isPreviewing: self.previewedItemId == itemSet.peer.id) } @@ -469,13 +714,24 @@ public final class StoryPeerListComponent: Component { self.visibleItems.removeValue(forKey: id) } - transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: collapsedContentOrigin, y: 6.0), size: CGSize(width: collapsedContentWidth, height: 44.0 - 4.0))) + transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: collapsedContentOrigin - 4.0, y: 6.0 - 59.0), size: CGSize(width: collapsedContentWidth + 4.0, height: 44.0))) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard let result = super.hitTest(point, with: event) else { + if self.alpha.isZero { return nil } + var result: UIView? + for view in self.subviews.reversed() { + if let resultValue = view.hitTest(self.convert(point, to: view), with: event), resultValue.isUserInteractionEnabled { + result = resultValue + } + } + + guard let result else { + return nil + } + if self.collapsedButton.isUserInteractionEnabled { if result !== self.collapsedButton { return nil @@ -489,6 +745,9 @@ public final class StoryPeerListComponent: Component { } func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var transition = transition + transition.animation = .none + if self.component != nil { if component.collapseFraction != 0.0 && self.scrollView.bounds.minX != 0.0 { self.ignoreScrolling = true @@ -503,6 +762,53 @@ public final class StoryPeerListComponent: Component { } } + let animationHint = transition.userData(AnimationHint.self) + var useAnimation = false + if let previousComponent = self.component, component.unlocked != previousComponent.unlocked { + useAnimation = true + } else if let animationHint, animationHint.allowAvatarsExpansionUpdated { + useAnimation = true + } + if let animationHint, animationHint.disableAnimations { + useAnimation = false + self.animationState = nil + } + + let timestamp = CACurrentMediaTime() + if let previousComponent = self.component, useAnimation { + let duration: Double + if let durationValue = animationHint?.duration { + duration = durationValue + } else if component.unlocked { + duration = 0.3 + } else { + duration = 0.25 + } + self.animationState = AnimationState(duration: duration * UIView.animationDurationFactor(), fromIsUnlocked: previousComponent.unlocked, fromFraction: self.currentFraction, startTime: timestamp, bounce: animationHint?.bounce ?? true) + } + + if let animationState = self.animationState { + if animationState.isFinished(at: timestamp) { + self.animationState = nil + } + } + + if let _ = self.animationState { + if self.animator == nil { + let animator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + }) + self.animator = animator + animator.isPaused = false + } + } else if let animator = self.animator { + self.animator = nil + animator.invalidate() + } + self.component = component self.state = state @@ -522,7 +828,7 @@ public final class StoryPeerListComponent: Component { } } - self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne + self.collapsedButton.isUserInteractionEnabled = !component.unlocked self.sortedItems.removeAll(keepingCapacity: true) if let storySubscriptions = component.storySubscriptions { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 0423ece9a8..29200a3680 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -269,10 +269,11 @@ public final class StoryPeerListItemComponent: Component { public let hasItems: Bool public let ringAnimation: RingAnimation? public let collapseFraction: CGFloat - public let collapsedScaleFactor: CGFloat + public let scale: CGFloat public let collapsedWidth: CGFloat - public let leftNeighborDistance: CGFloat? - public let rightNeighborDistance: CGFloat? + public let expandedAlphaFraction: CGFloat + public let leftNeighborDistance: CGPoint? + public let rightNeighborDistance: CGPoint? public let action: (EnginePeer) -> Void public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void @@ -285,10 +286,11 @@ public final class StoryPeerListItemComponent: Component { hasItems: Bool, ringAnimation: RingAnimation?, collapseFraction: CGFloat, - collapsedScaleFactor: CGFloat, + scale: CGFloat, collapsedWidth: CGFloat, - leftNeighborDistance: CGFloat?, - rightNeighborDistance: CGFloat?, + expandedAlphaFraction: CGFloat, + leftNeighborDistance: CGPoint?, + rightNeighborDistance: CGPoint?, action: @escaping (EnginePeer) -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void ) { @@ -300,8 +302,9 @@ public final class StoryPeerListItemComponent: Component { self.hasItems = hasItems self.ringAnimation = ringAnimation self.collapseFraction = collapseFraction - self.collapsedScaleFactor = collapsedScaleFactor + self.scale = scale self.collapsedWidth = collapsedWidth + self.expandedAlphaFraction = expandedAlphaFraction self.leftNeighborDistance = leftNeighborDistance self.rightNeighborDistance = rightNeighborDistance self.action = action @@ -333,12 +336,15 @@ public final class StoryPeerListItemComponent: Component { if lhs.collapseFraction != rhs.collapseFraction { return false } - if lhs.collapsedScaleFactor != rhs.collapsedScaleFactor { + if lhs.scale != rhs.scale { return false } if lhs.collapsedWidth != rhs.collapsedWidth { return false } + if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction { + return false + } if lhs.leftNeighborDistance != rhs.leftNeighborDistance { return false } @@ -516,7 +522,7 @@ public final class StoryPeerListItemComponent: Component { let effectiveWidth: CGFloat = (1.0 - component.collapseFraction) * availableSize.width + component.collapseFraction * component.collapsedWidth - let effectiveScale: CGFloat = 1.0 * (1.0 - component.collapseFraction) + (24.0 / 52.0) * component.collapsedScaleFactor * component.collapseFraction + let effectiveScale: CGFloat = component.scale let avatarNode: AvatarNode if let current = self.avatarNode { @@ -665,10 +671,10 @@ public final class StoryPeerListItemComponent: Component { var mappedRightCenter: CGPoint? if let leftNeighborDistance = component.leftNeighborDistance { - mappedLeftCenter = CGPoint(x: indicatorCenter.x - leftNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y) + mappedLeftCenter = CGPoint(x: indicatorCenter.x - leftNeighborDistance.x * (1.0 / effectiveScale), y: indicatorCenter.y + leftNeighborDistance.y * (1.0 / effectiveScale)) } if let rightNeighborDistance = component.rightNeighborDistance { - mappedRightCenter = CGPoint(x: indicatorCenter.x + rightNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y) + mappedRightCenter = CGPoint(x: indicatorCenter.x + rightNeighborDistance.x * (1.0 / effectiveScale), y: indicatorCenter.y + rightNeighborDistance.y * (1.0 / effectiveScale)) } let avatarPath = CGMutablePath() @@ -677,7 +683,7 @@ public final class StoryPeerListItemComponent: Component { let cutoutSize: CGFloat = 18.0 + UIScreenPixel * 2.0 avatarPath.addEllipse(in: CGRect(origin: CGPoint(x: avatarSize.width - cutoutSize + UIScreenPixel, y: avatarSize.height - 1.0 - cutoutSize + UIScreenPixel), size: CGSize(width: cutoutSize, height: cutoutSize))) } else if let mappedLeftCenter { - avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineWidth, dy: -indicatorLineWidth).offsetBy(dx: -abs(indicatorCenter.x - mappedLeftCenter.x), dy: 0.0)) + avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineWidth, dy: -indicatorLineWidth).offsetBy(dx: -abs(indicatorCenter.x - mappedLeftCenter.x), dy: -abs(indicatorCenter.y - mappedLeftCenter.y))) } Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath) @@ -700,10 +706,10 @@ public final class StoryPeerListItemComponent: Component { if let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) { self.button.addSubview(snapshotView) snapshotView.frame = titleView.frame - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView.layer.animateAlpha(from: component.expandedAlphaFraction, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) - titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + titleView.layer.animateAlpha(from: 0.0, to: component.expandedAlphaFraction, duration: 0.25) } titleTransition = .immediate @@ -741,7 +747,7 @@ public final class StoryPeerListItemComponent: Component { titleTransition.setPosition(view: titleView, position: titleFrame.center) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleTransition.setScale(view: titleView, scale: effectiveScale) - titleTransition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction) + titleTransition.setAlpha(view: titleView, alpha: component.expandedAlphaFraction) } if let ringAnimation = component.ringAnimation { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index bdb67b2561..1f9db5f2ae 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -105,7 +105,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { let chatListFilters = chatSelection.chatListFilters placeholder = placeholderValue - let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false) + let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _ in attemptDisabledItemSelection?(peer) @@ -263,7 +263,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { case let .contacts(contactsNode): contactsNode.scrollToTop() case let .chats(chatsNode): - chatsNode.scrollToPosition(.top) + chatsNode.scrollToPosition(.top(adjustForTempInset: false)) } } diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 83c264a2fd..ad0c4f1b99 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -210,7 +210,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode = nil } else { self.mainContainerNode = nil - self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false) + self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true) } super.init() @@ -1291,9 +1291,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { func scrollToTop() { if self.mainContainerNode?.supernode != nil { - self.mainContainerNode?.scrollToTop(animated: true) + self.mainContainerNode?.scrollToTop(animated: true, adjustForTempInset: false) } else if self.chatListNode?.supernode != nil { - self.chatListNode?.scrollToPosition(.top) + self.chatListNode?.scrollToPosition(.top(adjustForTempInset: false)) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { //contactListNode.scrollToTop() }