From 8e28d856266791b25eae5ddbd0a09e31ecadc276 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 12 May 2023 19:29:22 +0400 Subject: [PATCH] [WIP] Stories --- .../Sources/AccountContext.swift | 4 + .../AccountContext/Sources/FetchManager.swift | 9 +- .../Sources/ChatListController.swift | 194 +++++---- .../Sources/ChatListControllerNode.swift | 6 +- .../Sources/Node/ChatListNode.swift | 22 +- submodules/Display/Source/NavigationBar.swift | 6 +- .../Sources/FetchManagerImpl.swift | 7 +- .../NavigationBarSearchContentNode.swift | 7 +- .../TelegramEngine/Messages/Stories.swift | 119 ++++++ .../Messages/StoryListContext.swift | 87 +++- .../Components/ChatListHeaderComponent/BUILD | 2 + .../Sources/ChatListHeaderComponent.swift | 214 +++++++++- .../StoryItemSetContainerComponent.swift | 4 - .../Sources/StoryChatContent.swift | 3 +- .../Sources/StoryItemContentComponent.swift | 49 ++- .../Sources/StoryPeerListComponent.swift | 236 ++++++++++- .../Sources/StoryPeerListItemComponent.swift | 381 ++++++++++++++++-- .../Sources/TelegramRootController.swift | 50 ++- 18 files changed, 1216 insertions(+), 184 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1cd2832e98..4d3c0a6bad 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -742,6 +742,10 @@ public protocol RecentSessionsController: AnyObject { public protocol AttachmentFileController: AnyObject { } +public protocol TelegramRootControllerInterface: NavigationController { + func openStoryCamera() +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } diff --git a/submodules/AccountContext/Sources/FetchManager.swift b/submodules/AccountContext/Sources/FetchManager.swift index 5c2c64714b..39ca3de10c 100644 --- a/submodules/AccountContext/Sources/FetchManager.swift +++ b/submodules/AccountContext/Sources/FetchManager.swift @@ -86,8 +86,15 @@ public struct FetchManagerPriorityKey: Comparable { } } -public enum FetchManagerLocation: Hashable { +public enum FetchManagerLocation: Hashable, CustomStringConvertible { case chat(PeerId) + + public var description: String { + switch self { + case let .chat(peerId): + return "chat:\(peerId)" + } + } } public enum FetchManagerForegroundDirection { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index bb3603a82c..bb88d23660 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -47,12 +47,46 @@ import InviteLinksUI import ChatFolderLinkPreviewScreen import StoryContainerScreen import StoryContentComponent -import StoryPeerListComponent private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { return false } + + let storiesFraction = 94.0 / (navigationBarSearchContentHeight + 94.0) + + var visibleStoriesProgress = max(0.0, min(1.0, searchNode.expansionProgress)) + visibleStoriesProgress = (1.0 / storiesFraction) * visibleStoriesProgress + visibleStoriesProgress = max(0.0, min(1.0, visibleStoriesProgress)) + + let searchFieldHeight: CGFloat = 36.0 + let searchFraction = searchFieldHeight / searchNode.nominalHeight + let visibleSearchProgress = max(0.0, min(1.0, searchNode.expansionProgress) - 1.0 + searchFraction) / searchFraction + + if visibleSearchProgress > 0.0 && visibleSearchProgress < 1.0 { + let offset: CGFloat + if visibleSearchProgress < 0.6 { + offset = navigationBarSearchContentHeight + } else { + offset = 0.0 + } + let _ = listNode.scrollToOffsetFromTop(offset, animated: true) + return true + } else if visibleStoriesProgress > 0.0 && visibleStoriesProgress < 1.0 { + let offset: CGFloat + if visibleStoriesProgress < 0.3 { + offset = navigationBarSearchContentHeight + 94.0 + } else { + offset = navigationBarSearchContentHeight + } + let _ = listNode.scrollToOffsetFromTop(offset, animated: true) + return true + } + + if "".isEmpty { + return false + } + if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 { let offset: CGFloat if searchNode.expansionProgress < 0.6 { @@ -185,7 +219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var searchContentNode: NavigationBarSearchContentNode? private let navigationSecondaryContentNode: ASDisplayNode - private var storyPeerListView: ComponentView? private let tabContainerNode: ChatListFilterTabContainerNode private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)? @@ -210,7 +243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var powerSavingMonitoringDisposable: Disposable? - private var storyListContext: StoryListContext? + public private(set) var storyListContext: StoryListContext? private var storyListState: StoryListContext.State? private var storyListStateDisposable: Disposable? @@ -444,7 +477,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController tabsIsEmpty = true } - self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight + if case .chatList(.root) = self.location { + self.searchContentNode?.additionalHeight = 94.0 + } + self.navigationBar?.secondaryContentHeight = !tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0 enum State: Equatable { case empty(hasDownloads: Bool) @@ -2123,12 +2159,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController tabsIsEmpty = true } - self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight + self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + if case .chatList(.root) = self.location { + self.searchContentNode?.additionalHeight = 94.0 + } - if wasEmpty != isEmpty || self.storyPeerListView == nil { - self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) - } else if let componentView = self.storyPeerListView?.view, !componentView.bounds.isEmpty { - self.updateStoryPeerListView(size: componentView.bounds.size, transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + if wasEmpty != isEmpty { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition + self.requestLayout(transition: transition) + self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil + } else { + self.updateHeaderStories(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).containedViewLayoutTransition) } }) } @@ -2358,33 +2400,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController super.updateNavigationBarLayout(layout, transition: transition) } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false - - self.validLayout = layout - - self.updateLayout(layout: layout, transition: transition) - - if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver { - searchContentNode.updateListVisibleContentOffset(.known(0.0)) - self.chatListDisplayNode.scrollToTop() - } - } - - private func updateStoryPeerListView(size: CGSize, transition: Transition) { - guard let storyPeerListView = self.storyPeerListView else { - return - } - let _ = storyPeerListView.update( - transition: transition, - component: AnyComponent(StoryPeerListComponent( - context: self.context, - theme: self.presentationData.theme, - strings: self.presentationData.strings, - state: self.storyListState, - peerAction: { [weak self] peer in + private func updateHeaderStories(transition: ContainedViewLayoutTransition) { + if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location { + if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + componentView.storyPeerAction = { [weak self] peer in guard let self, let storyListContext = self.storyListContext else { return } @@ -2401,8 +2420,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var transitionIn: StoryContainerScreen.TransitionIn? - if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View { - if let transitionView = storyPeerListView.transitionViewForItem(peerId: peer.id) { + if let peer, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, @@ -2411,9 +2430,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + var initialFocusedId: AnyHashable? + if let peer { + initialFocusedId = AnyHashable(peer.id) + } + + if !initialContent.contains(where: { slice in + return !slice.items.isEmpty + }) { + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openStoryCamera() + } + + return + } + let storyContainerScreen = StoryContainerScreen( context: self.context, - initialFocusedId: AnyHashable(peer.id), + initialFocusedId: initialFocusedId, initialContent: initialContent, transitionIn: transitionIn, transitionOut: { [weak self] peerId in @@ -2421,8 +2455,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } - if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View { - if let transitionView = storyPeerListView.transitionViewForItem(peerId: peerId) { + if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryContainerScreen.TransitionOut( destinationView: transitionView, destinationRect: transitionView.bounds, @@ -2437,10 +2471,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.push(storyContainerScreen) }) } - )), - environment: {}, - containerSize: size - ) + + let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0) + + var visibleProgress = max(0.0, min(1.0, searchContentNode.expansionProgress)) + visibleProgress = (1.0 / fraction) * visibleProgress + visibleProgress = max(0.0, min(1.0, visibleProgress)) + + componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storyListState: self.storyListState, transition: Transition(transition)) + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false + + self.validLayout = layout + + self.updateLayout(layout: layout, transition: transition) + + self.updateHeaderStories(transition: transition) + + if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver { + searchContentNode.updateListVisibleContentOffset(.known(0.0)) + self.chatListDisplayNode.scrollToTop() + } } private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -2457,35 +2514,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: layout.size.width, height: 46.0))) - if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty { - var storyPeerListTransition = Transition(transition) - let storyPeerListView: ComponentView - if let current = self.storyPeerListView { - storyPeerListView = current - } else { - storyPeerListTransition = .immediate - storyPeerListView = ComponentView() - self.storyPeerListView = storyPeerListView - } - let storyListFrame = CGRect(origin: CGPoint(x: 0.0, y: 46.0 - 0.0), size: CGSize(width: layout.size.width, height: self.storyListHeight + 0.0)) - self.updateStoryPeerListView(size: storyListFrame.size, transition: storyPeerListTransition) - if let componentView = storyPeerListView.view { - if componentView.superview == nil { - componentView.alpha = 0.0 - self.navigationSecondaryContentNode.view.addSubview(componentView) - } - storyPeerListTransition.setFrame(view: componentView, frame: storyListFrame) - transition.updateAlpha(layer: componentView.layer, alpha: 1.0) - } - } else if let storyPeerListView = self.storyPeerListView { - self.storyPeerListView = nil - if let componentView = storyPeerListView.view { - transition.updateAlpha(layer: componentView.layer, alpha: 0.0, completion: { [weak componentView] _ in - componentView?.removeFromSuperview() - }) - } - } - if !skipTabContainerUpdate { self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) } @@ -2930,7 +2958,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.didSetupTabs = true if strongSelf.displayNavigationBar { - strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + strongSelf.storyListHeight + strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + if case .chatList(.root) = strongSelf.location { + strongSelf.searchContentNode?.additionalHeight = 94.0 + } strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false) } @@ -3309,6 +3340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false) } + strongSelf.searchContentNode?.additionalHeight = 0.0 activate(filter != .downloads) @@ -3362,7 +3394,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController filterContainerNode = searchContentNode.filterContainerNode if let filterContainerNode = filterContainerNode, let snapshotView = filterContainerNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = filterContainerNode.frame + snapshotView.frame = filterContainerNode.frame//.offsetBy(dx: self.navigationSecondaryContentNode.frame.minX, dy: self.navigationSecondaryContentNode.frame.minY) filterContainerNode.view.superview?.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in @@ -3379,10 +3411,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let searchContentNode = self.searchContentNode { + let previousFrame = searchContentNode.placeholderNode.frame + if case .chatList(.root) = self.location { + searchContentNode.placeholderNode.frame = previousFrame.offsetBy(dx: 0.0, dy: 94.0) + } completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) + searchContentNode.placeholderNode.frame = previousFrame } - self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight + self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + if case .chatList(.root) = self.location { + self.searchContentNode?.additionalHeight = 94.0 + } self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index ef573294fd..be1c1b7053 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -373,6 +373,10 @@ private final class ChatListContainerItemNode: ASDisplayNode { 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) + if let controller, case .chatList(groupId: .root) = controller.location { + self.listNode.scrollHeightTopInset = navigationBarSearchContentHeight + 94.0 + } + super.init() self.addSubnode(self.listNode) @@ -1481,7 +1485,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0 private(set) var inlineStackContainerNode: ChatListContainerNode? private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer? - private(set) var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition? + var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition? private var tapRecognizer: UITapGestureRecognizer? var navigationBar: NavigationBar? diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 410bebddd1..bd931981fc 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1184,6 +1184,12 @@ public final class ChatListNode: ListView { private var pollFilterUpdatesDisposable: Disposable? private var chatFilterUpdatesDisposable: Disposable? + public var scrollHeightTopInset: CGFloat { + didSet { + self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset + } + } + 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) { self.context = context self.location = location @@ -1204,12 +1210,14 @@ public final class ChatListNode: ListView { self.theme = theme + self.scrollHeightTopInset = navigationBarSearchContentHeight + super.init() self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.verticalScrollIndicatorFollowsOverscroll = true - self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight + self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { @@ -3140,7 +3148,7 @@ public final class ChatListNode: ListView { case let .known(value) where abs(value) < .ulpOfOne: offset = 0.0 default: - offset = -navigationBarSearchContentHeight + offset = -self.scrollHeightTopInset } } scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) @@ -3153,7 +3161,7 @@ public final class ChatListNode: ListView { var isNavigationHidden: Bool { switch self.visibleContentOffset() { - case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0: + case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0: return false case .none: return false @@ -3165,11 +3173,11 @@ public final class ChatListNode: ListView { var isNavigationInAFinalState: Bool { switch self.visibleContentOffset() { case let .known(value): - if value < navigationBarSearchContentHeight - 1.0 { + if value < self.scrollHeightTopInset - 1.0 { if abs(value - 0.0) < 1.0 { return true } - if abs(value - navigationBarSearchContentHeight) < 1.0 { + if abs(value - self.scrollHeightTopInset) < 1.0 { return true } return false @@ -3187,9 +3195,9 @@ public final class ChatListNode: ListView { } var scrollToItem: ListViewScrollToItem? switch self.visibleContentOffset() { - case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0: + case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0: if isNavigationHidden { - scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) + scrollToItem = ListViewScrollToItem(index: 0, position: .top(-self.scrollHeightTopInset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } default: if !isNavigationHidden { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index b43cdc2d90..2ee83fe738 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1395,7 +1395,6 @@ open class NavigationBar: ASDisplayNode { if let customHeaderContentView = self.customHeaderContentView { let headerSize = CGSize(width: size.width, height: nominalHeight) - //customHeaderContentView.update(size: headerSize, transition: transition) transition.updateFrame(view: customHeaderContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOrigin), size: headerSize)) } @@ -1732,12 +1731,15 @@ open class NavigationBar: ASDisplayNode { if let result = self.additionalContentNode.view.hitTest(self.view.convert(point, to: self.additionalContentNode.view), with: event) { return result } + + if self.frame.minY > -10.0, let customHeaderContentView = self.customHeaderContentView, let result = customHeaderContentView.hitTest(self.view.convert(point, to: customHeaderContentView), with: event) { + return result + } guard let result = super.hitTest(point, with: event) else { return nil } - if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) { return nil } diff --git a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift index 85181dc3b1..b53ba8f5d9 100644 --- a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift +++ b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift @@ -161,6 +161,9 @@ private final class FetchManagerCategoryContext { previousPriorityKey = nil let (mediaReference, resourceReference, statsCategory, episode) = takeNew() entry = FetchManagerLocationEntry(id: id, episode: episode, mediaReference: mediaReference, resourceReference: resourceReference, statsCategory: statsCategory) + + Logger.shared.log("FetchManager", "[\(entry.id.location)] Adding entry \(entry.resourceReference.resource.id.stringRepresentation) (\(self.entries.count) in queue)") + self.entries[id] = entry } else { return @@ -236,7 +239,7 @@ private final class FetchManagerCategoryContext { } activeContext.disposable?.dispose() let postbox = self.postbox - Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") + Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") var userLocation: MediaResourceUserLocation = .other switch entry.id.location { @@ -401,7 +404,7 @@ private final class FetchManagerCategoryContext { } else if ranges.isEmpty { } else { let postbox = self.postbox - Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") + Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") var userLocation: MediaResourceUserLocation = .other switch entry.id.location { diff --git a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift index 932a7a1e83..a8d4231712 100644 --- a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift +++ b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift @@ -19,6 +19,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { private var disabledOverlay: ASDisplayNode? public private(set) var expansionProgress: CGFloat = 1.0 + + public var additionalHeight: CGFloat = 0.0 private var validLayout: (CGSize, CGFloat, CGFloat)? @@ -125,7 +127,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, textColor, fillColor, backgroundColor, transition) searchBarApply() - let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: 8.0 + overscrollProgress * fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight)) + let _ = overscrollProgress + let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: size.height + (1.0 - visibleProgress) * fieldHeight - 8.0 - fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight)) transition.updateFrame(node: self.placeholderNode, frame: searchBarFrame) self.placeholderHeight = searchBarHeight @@ -151,7 +154,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { } override public var nominalHeight: CGFloat { - return navigationBarSearchContentHeight + return navigationBarSearchContentHeight + self.additionalHeight } override public var mode: NavigationBarContentMode { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 69df816aba..7cf8c8e654 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -5,6 +5,7 @@ import TelegramApi public enum EngineStoryInputMedia { case image(dimensions: PixelDimensions, data: Data) + case video(dimensions: PixelDimensions, duration: Int, resource: TelegramMediaResource) } public struct EngineStoryPrivacy: Equatable { @@ -141,6 +142,124 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, priva } |> switchToLatest } + case let .video(dimensions, duration, resource): + let fileMedia = TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: MediaId.Id.random(in: MediaId.Id.min ... MediaId.Id.max)), + partialReference: nil, + resource: resource, + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "video/mp4", + size: nil, + attributes: [ + TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming) + ] + ) + + let contentToUpload = messageContentToUpload( + network: account.network, + postbox: account.postbox, + auxiliaryMethods: account.auxiliaryMethods, + transformOutgoingMessageMedia: nil, + messageMediaPreuploadManager: account.messageMediaPreuploadManager, + revalidationContext: account.mediaReferenceRevalidationContext, + forceReupload: true, + isGrouped: false, + peerId: account.peerId, + messageId: nil, + attributes: [], + text: "", + media: [fileMedia] + ) + let contentSignal: Signal + switch contentToUpload { + case let .immediate(result, _): + contentSignal = .single(result) + case let .signal(signal, _): + contentSignal = signal + } + + return contentSignal + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Signal in + var privacyRules: [Api.InputPrivacyRule] + switch privacy.base { + case .everyone: + privacyRules = [.inputPrivacyValueAllowAll] + case .contacts: + privacyRules = [.inputPrivacyValueAllowContacts] + case .closeFriends: + privacyRules = [.inputPrivacyValueAllowCloseFriends] + } + var privacyUsers: [Api.InputUser] = [] + var privacyChats: [Int64] = [] + for peerId in privacy.additionallyIncludePeers { + if let peer = transaction.getPeer(peerId) { + if let _ = peer as? TelegramUser { + if let inputUser = apiInputUser(peer) { + privacyUsers.append(inputUser) + } + } else if peer is TelegramGroup || peer is TelegramChannel { + privacyChats.append(peer.id.id._internalGetInt64Value()) + } + } + } + if !privacyUsers.isEmpty { + privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers)) + } + if !privacyChats.isEmpty { + privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats)) + } + + switch result { + case let .content(content): + switch content.content { + case let .media(inputMedia, _): + return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: privacyRules)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + for update in updates.allUpdates { + if case let .updateStories(stories) = update { + switch stories { + case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _): + if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 { + switch apiStories[0] { + case let .storyItem(_, _, _, _, _, media, _, _, _): + let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) + if let parsedMedia = parsedMedia { + applyMediaResourceChanges(from: fileMedia, to: parsedMedia, postbox: account.postbox, force: true) + } + default: + break + } + } + } + } + } + + account.stateManager.addUpdates(updates) + } + + return .complete() + } + default: + return .complete() + } + default: + return .complete() + } + } + |> switchToLatest + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index b4623bc03d..8ac8c25fe3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -107,14 +107,23 @@ public final class StoryListContext { public struct State: Equatable { public var itemSets: [PeerItemSet] + public var uploadProgress: CGFloat? public var loadMoreToken: LoadMoreToken? - public init(itemSets: [PeerItemSet], loadMoreToken: LoadMoreToken?) { + public init(itemSets: [PeerItemSet], uploadProgress: CGFloat?, loadMoreToken: LoadMoreToken?) { self.itemSets = itemSets + self.uploadProgress = uploadProgress self.loadMoreToken = loadMoreToken } } + private final class UploadContext { + let disposable = MetaDisposable() + + init() { + } + } + private final class Impl { private let queue: Queue private let account: Account @@ -127,6 +136,12 @@ public final class StoryListContext { private var updatesDisposable: Disposable? private var peerDisposables: [PeerId: Disposable] = [:] + private var uploadContexts: [UploadContext] = [] { + didSet { + self.stateValue.uploadProgress = self.uploadContexts.isEmpty ? nil : 0.0 + } + } + private var stateValue: State { didSet { self.state.set(.single(self.stateValue)) @@ -139,9 +154,21 @@ public final class StoryListContext { self.account = account self.scope = scope - self.stateValue = State(itemSets: [], loadMoreToken: LoadMoreToken(value: nil)) + self.stateValue = State(itemSets: [], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil)) self.state.set(.single(self.stateValue)) + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(account.peerId) + } + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.stateValue = State(itemSets: [ + PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0) + ], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil)) + }) + self.updatesDisposable = (account.stateManager.storyUpdates |> deliverOn(queue)).start(next: { [weak self] updates in if updates.isEmpty { @@ -150,6 +177,11 @@ public final class StoryListContext { let _ = account.postbox.transaction({ transaction -> [PeerId: Peer] in var peers: [PeerId: Peer] = [:] + + if let peer = transaction.getPeer(account.peerId) { + peers[peer.id] = peer + } + for update in updates { switch update { case let .added(peerId, _): @@ -198,7 +230,21 @@ public final class StoryListContext { if let index = items.firstIndex(where: { $0.id == item.id }) { items.remove(at: index) } - items.append(item) + + if peerId == self.account.peerId { + items.append(Item( + id: item.id, + timestamp: item.timestamp, + media: item.media, + isSeen: false, + seenCount: item.seenCount, + seenPeers: item.seenPeers, + privacy: item.privacy + )) + } else { + items.append(item) + } + items.sort(by: { lhsItem, rhsItem in if lhsItem.timestamp != rhsItem.timestamp { return lhsItem.timestamp < rhsItem.timestamp @@ -263,6 +309,12 @@ public final class StoryListContext { return lhsItem.id > rhsItem.id }) + if !itemSets.contains(where: { $0.peerId == self.account.peerId }) { + if let peer = peers[self.account.peerId] { + itemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0) + } + } + self.stateValue.itemSets = itemSets }) }) @@ -398,6 +450,21 @@ public final class StoryListContext { } } + func upload(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) { + let uploadContext = UploadContext() + self.uploadContexts.append(uploadContext) + uploadContext.disposable.set((_internal_uploadStory(account: self.account, media: media, privacy: privacy) + |> deliverOn(self.queue)).start(next: { _ in + }, completed: { [weak self, weak uploadContext] in + guard let `self` = self, let uploadContext = uploadContext else { + return + } + if let index = self.uploadContexts.firstIndex(where: { $0 === uploadContext }) { + self.uploadContexts.remove(at: index) + } + })) + } + func loadMore(refresh: Bool) { if self.isLoadingMore { return @@ -528,6 +595,12 @@ public final class StoryListContext { } } + if !parsedItemSets.contains(where: { $0.peerId == account.peerId }) { + if let peer = transaction.getPeer(account.peerId) { + parsedItemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0) + } + } + return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) }) } } @@ -589,7 +662,7 @@ public final class StoryListContext { return lhsItem.id > rhsItem.id }) - self.stateValue = State(itemSets: itemSets, loadMoreToken: result.1) + self.stateValue = State(itemSets: itemSets, uploadProgress: self.stateValue.uploadProgress, loadMoreToken: result.1) })) } @@ -639,4 +712,10 @@ public final class StoryListContext { impl.delete(id: id) } } + + public func upload(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) { + self.impl.with { impl in + impl.upload(media: media, privacy: privacy) + } + } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index ab8fd7628c..e3178438da 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -16,9 +16,11 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/TelegramUI/Components/ChatListTitleView", "//submodules/AccountContext", + "//submodules/TelegramCore", "//submodules/AppBundle", "//submodules/AsyncDisplayKit", "//submodules/AnimationUI", + "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 20a1310511..b97ee5c3e4 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -6,6 +6,8 @@ import TelegramPresentationData import AccountContext import ChatListTitleView import AppBundle +import StoryPeerListComponent +import TelegramCore public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { @@ -272,10 +274,13 @@ public final class ChatListHeaderComponent: Component { var backButtonView: BackButtonView? let titleOffsetContainer: UIView + let titleScaleContainer: UIView let titleTextView: ImmediateTextView var titleContentView: ComponentView? var chatListTitleView: ChatListTitleView? + var contentOffsetFraction: CGFloat = 0.0 + init( backPressed: @escaping () -> Void, openStatusSetup: @escaping (UIView) -> Void, @@ -288,16 +293,18 @@ public final class ChatListHeaderComponent: Component { self.leftButtonOffsetContainer = UIView() self.rightButtonOffsetContainer = UIView() self.titleOffsetContainer = UIView() + self.titleScaleContainer = UIView() self.titleTextView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.titleOffsetContainer) + self.titleOffsetContainer.addSubview(self.titleScaleContainer) self.addSubview(self.leftButtonOffsetContainer) self.addSubview(self.rightButtonOffsetContainer) - self.titleOffsetContainer.addSubview(self.titleTextView) + self.titleScaleContainer.addSubview(self.titleTextView) } required init?(coder: NSCoder) { @@ -329,10 +336,28 @@ public final class ChatListHeaderComponent: Component { return nil } + func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: Transition) { + if self.contentOffsetFraction == contentOffsetFraction { + return + } + self.contentOffsetFraction = contentOffsetFraction + + let scale = 1.0 * (1.0 - contentOffsetFraction) + 0.001 * contentOffsetFraction + let translation = -44.0 * contentOffsetFraction * 0.5 + + var transform = CATransform3DIdentity + transform = CATransform3DTranslate(transform, 0.0, translation, 0.0) + transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) + + transition.setScale(view: self.titleScaleContainer, scale: scale) + transition.setAlpha(view: self.titleScaleContainer, alpha: 1.0 - contentOffsetFraction) + } + func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in completion() }) + transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) if let backButtonView = self.backButtonView { @@ -356,6 +381,7 @@ public final class ChatListHeaderComponent: Component { transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) if let backButtonView = self.backButtonView { transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) @@ -373,7 +399,45 @@ public final class ChatListHeaderComponent: Component { } } + func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in + }) + transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in + completion() + }) + + if let backButtonView = self.backButtonView { + transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in + }) + } + + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + } + + func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in + completion() + }) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) + transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) + if let backButtonView = self.backButtonView { + transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) + transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) + + transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) + } + } + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, size: CGSize, transition: Transition) { + transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) + + transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size)) + self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) let buttonSpacing: CGFloat = 8.0 @@ -530,7 +594,7 @@ public final class ChatListHeaderComponent: Component { ) if let titleContentComponentView = titleContentView.view { if titleContentComponentView.superview == nil { - self.titleOffsetContainer.addSubview(titleContentComponentView) + self.titleScaleContainer.addSubview(titleContentComponentView) } titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize)) } @@ -551,7 +615,7 @@ public final class ChatListHeaderComponent: Component { chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer) chatListTitleView.manualLayout = true self.chatListTitleView = chatListTitleView - self.titleOffsetContainer.addSubview(chatListTitleView) + self.titleScaleContainer.addSubview(chatListTitleView) } let chatListTitleContentSize = size @@ -591,6 +655,10 @@ public final class ChatListHeaderComponent: Component { private var primaryContentView: ContentView? private var secondaryContentView: ContentView? + private var storyOffsetFraction: CGFloat = 0.0 + + private var storyPeerList: ComponentView? + public var storyPeerAction: ((EnginePeer?) -> Void)? private var effectiveContentView: ContentView? { return self.secondaryContentView ?? self.primaryContentView @@ -598,6 +666,8 @@ public final class ChatListHeaderComponent: Component { override init(frame: CGRect) { super.init(frame: frame) + + self.storyOffsetFraction = 1.0 } required public init?(coder: NSCoder) { @@ -643,6 +713,107 @@ public final class ChatListHeaderComponent: Component { } } + public func storyPeerListView() -> StoryPeerListComponent.View? { + return self.storyPeerList?.view as? StoryPeerListComponent.View + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews.reversed() { + if !subview.isUserInteractionEnabled || subview.alpha < 0.01 || subview.isHidden { + continue + } + if subview === self.storyPeerList?.view { + continue + } + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + return result + } + } + + if let storyPeerListView = self.storyPeerList?.view { + if let result = storyPeerListView.hitTest(self.convert(point, to: storyPeerListView), with: event) { + return result + } + } + + let defaultResult = super.hitTest(point, with: event) + + if let defaultResult, defaultResult !== self { + return defaultResult + } + + return defaultResult + } + + public func updateStories(offset: CGFloat, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, storyListState: StoryListContext.State?, transition: Transition) { + var storyOffsetFraction: CGFloat = 1.0 + if let storyListState, storyListState.itemSets.count > 1 { + storyOffsetFraction = offset + } + + self.storyOffsetFraction = storyOffsetFraction + + let storyPeerList: ComponentView + var storyListTransition = transition + if let current = self.storyPeerList { + storyPeerList = current + } else { + storyListTransition = .immediate + storyPeerList = ComponentView() + self.storyPeerList = storyPeerList + } + + if !self.bounds.isEmpty { + let _ = storyPeerList.update( + transition: storyListTransition, + component: AnyComponent(StoryPeerListComponent( + context: context, + theme: theme, + strings: strings, + state: storyListState, + collapseFraction: 1.0 - offset, + peerAction: { [weak self] peer in + guard let self else { + return + } + self.storyPeerAction?(peer) + } + )), + environment: {}, + containerSize: CGSize(width: self.bounds.width, height: 94.0) + ) + if let storyPeerListComponentView = storyPeerList.view { + if storyPeerListComponentView.superview == nil { + self.addSubview(storyPeerListComponentView) + } + + let storyPeerListMinOffset: CGFloat = -7.0 + let storyPeerListMaxOffset: CGFloat = self.bounds.height + 8.0 + + let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - offset) + storyPeerListMaxOffset * offset + + storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: storyPeerListPosition), size: CGSize(width: self.bounds.width, height: 94.0))) + + var storyListAlpha: CGFloat = 1.0 + if let storyListState, storyListState.itemSets.count > 1 { + } else { + storyListAlpha = offset + } + storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) + } + + if let primaryContentView = self.primaryContentView { + primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition) + } + if let secondaryContentView = self.secondaryContentView { + secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition) + } + } + } + + private func updateContentStoryOffsets(transition: Transition) { + } + func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.state = state @@ -681,6 +852,8 @@ public final class ChatListHeaderComponent: Component { } primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) } else if let primaryContentView = self.primaryContentView { self.primaryContentView = nil primaryContentView.removeFromSuperview() @@ -719,23 +892,42 @@ public final class ChatListHeaderComponent: Component { secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, size: availableSize, transition: secondaryContentTransition) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) + if let primaryContentView = self.primaryContentView { if let previousComponent = previousComponent, previousComponent.secondaryContent == nil { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) - secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + if self.storyOffsetFraction < 0.8 { + primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + } else { + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + } } - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) - secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + if self.storyOffsetFraction < 0.8 { + primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + } else { + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + } } } else if let secondaryContentView = self.secondaryContentView { self.secondaryContentView = nil if let primaryContentView = self.primaryContentView { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) - secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in - secondaryContentView?.removeFromSuperview() - }) + if self.storyOffsetFraction < 0.8 { + primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.removeFromSuperview() + }) + } else { + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.removeFromSuperview() + }) + } } else { secondaryContentView.removeFromSuperview() } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 7d2eafd07a..699109e035 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -687,10 +687,6 @@ public final class StoryItemSetContainerComponent: Component { self.currentSliceDisposable?.dispose() if let focusedItemId = self.focusedItemId { - if let item = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) { - item.markAsSeen?() - } - self.currentSliceDisposable = (component.initialItemSlice.update( component.initialItemSlice, focusedItemId diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 7f32ede4be..dd5cadf49f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -24,6 +24,7 @@ public enum StoryChatContent { position: items.count, component: AnyComponent(StoryItemContentComponent( context: context, + peerId: peerId, item: item )), centerInfoComponent: AnyComponent(StoryAuthorInfoComponent( @@ -57,7 +58,7 @@ public enum StoryChatContent { var sliceFocusedItemId: AnyHashable? if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) { sliceFocusedItemId = AnyHashable(focusItem) - } else if itemSet.peerId != context.account.peerId { + } else { if let id = itemSet.items.first(where: { !$0.isSeen })?.id { sliceFocusedItemId = AnyHashable(id) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index f8552d2cdd..3302cc94ff 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -16,10 +16,12 @@ final class StoryItemContentComponent: Component { typealias EnvironmentType = StoryContentItem.Environment let context: AccountContext + let peerId: EnginePeer.Id let item: StoryListContext.Item - init(context: AccountContext, item: StoryListContext.Item) { + init(context: AccountContext, peerId: EnginePeer.Id, item: StoryListContext.Item) { self.context = context + self.peerId = peerId self.item = item } @@ -27,6 +29,9 @@ final class StoryItemContentComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.peerId != rhs.peerId { + return false + } if lhs.item != rhs.item { return false } @@ -99,6 +104,9 @@ final class StoryItemContentComponent: Component { private var currentProgressTimerValue: Double = 0.0 private var videoProgressDisposable: Disposable? + private var markedAsSeen: Bool = false + private var contentLoaded: Bool = false + private var videoPlaybackStatus: MediaPlayerStatus? private let hierarchyTrackingLayer: HierarchyTrackingLayer @@ -186,7 +194,7 @@ final class StoryItemContentComponent: Component { private func updateIsProgressPaused() { if let videoNode = self.videoNode { - if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy { + if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy { videoNode.play() } else { videoNode.pause() @@ -198,7 +206,7 @@ final class StoryItemContentComponent: Component { } private func updateProgressTimer() { - let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy + let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy if needsTimer { if self.currentProgressTimer == nil { @@ -206,13 +214,20 @@ final class StoryItemContentComponent: Component { timeout: 1.0 / 60.0, repeat: true, completion: { [weak self] in - guard let self, !self.isProgressPaused, self.hierarchyTrackingLayer.isInHierarchy else { + guard let self, !self.isProgressPaused, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else { return } - if self.videoNode != nil { + if case .file = self.currentMessageMedia { self.updateVideoPlaybackProgress() } else { + if !self.markedAsSeen { + self.markedAsSeen = true + if let component = self.component { + let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start() + } + } + #if DEBUG && false let currentProgressTimerLimit: Double = 5 * 60.0 #else @@ -273,6 +288,15 @@ final class StoryItemContentComponent: Component { progress = min(1.0, progress) currentProgress = progress + + if isPlaying { + if !self.markedAsSeen { + self.markedAsSeen = true + if let component = self.component { + let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start() + } + } + } } let clippedProgress = max(0.0, min(1.0, currentProgress)) @@ -325,6 +349,8 @@ final class StoryItemContentComponent: Component { } } case let .file(file): + self.contentLoaded = true + signal = chatMessageVideo( postbox: component.context.account.postbox, userLocation: .other, @@ -362,7 +388,15 @@ final class StoryItemContentComponent: Component { self.fetchDisposable?.dispose() self.fetchDisposable = nil if let fetchSignal { - self.fetchDisposable = fetchSignal.start() + self.fetchDisposable = (fetchSignal |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + if !self.contentLoaded { + self.contentLoaded = true + self.state?.updated(transition: .immediate) + } + }) } } @@ -408,7 +442,8 @@ final class StoryItemContentComponent: Component { }) } } - self.updateProgressTimer() + + self.updateIsProgressPaused() return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 67798eda76..210f279d0b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -14,19 +14,22 @@ public final class StoryPeerListComponent: Component { public let theme: PresentationTheme public let strings: PresentationStrings public let state: StoryListContext.State? - public let peerAction: (EnginePeer) -> Void + public let collapseFraction: CGFloat + public let peerAction: (EnginePeer?) -> Void public init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, state: StoryListContext.State?, - peerAction: @escaping (EnginePeer) -> Void + collapseFraction: CGFloat, + peerAction: @escaping (EnginePeer?) -> Void ) { self.context = context self.theme = theme self.strings = strings self.state = state + self.collapseFraction = collapseFraction self.peerAction = peerAction } @@ -43,6 +46,9 @@ public final class StoryPeerListComponent: Component { if lhs.state != rhs.state { return false } + if lhs.collapseFraction != rhs.collapseFraction { + return false + } return true } @@ -90,6 +96,7 @@ public final class StoryPeerListComponent: Component { } public final class View: UIView, UIScrollViewDelegate { + private let collapsedButton: HighlightableButton private let scrollView: ScrollView private var ignoreScrolling: Bool = false @@ -98,10 +105,14 @@ public final class StoryPeerListComponent: Component { private var sortedItemSets: [StoryListContext.PeerItemSet] = [] private var visibleItems: [EnginePeer.Id: VisibleItem] = [:] + private let title = ComponentView() + private var component: StoryPeerListComponent? private weak var state: EmptyComponentState? public override init(frame: CGRect) { + self.collapsedButton = HighlightableButton() + self.scrollView = ScrollView() self.scrollView.delaysContentTouches = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -116,13 +127,43 @@ public final class StoryPeerListComponent: Component { self.scrollView.delegate = self self.addSubview(self.scrollView) + self.addSubview(self.collapsedButton) + + self.collapsedButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.layer.allowsGroupOpacity = true + self.alpha = 0.6 + } else { + self.alpha = 1.0 + self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.25, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.layer.allowsGroupOpacity = false + }) + } + } + self.collapsedButton.addTarget(self, action: #selector(self.collapsedButtonPressed), for: .touchUpInside) } required public init?(coder: NSCoder) { preconditionFailure() } + @objc private func collapsedButtonPressed() { + guard let component = self.component else { + return + } + component.peerAction(nil) + } + public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? { + if self.collapsedButton.isUserInteractionEnabled { + return nil + } if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { return itemView.transitionView() } @@ -131,21 +172,106 @@ public final class StoryPeerListComponent: Component { public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) + self.updateScrolling(transition: .immediate, keepVisibleUntilCompletion: false) } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: Transition, keepVisibleUntilCompletion: Bool) { guard let component = self.component, let itemLayout = self.itemLayout else { return } + var hasStories: Bool = false + if let state = component.state, state.itemSets.count > 1 { + hasStories = true + } + + let titleSpacing: CGFloat = 8.0 + + let titleText: String + let storyCount = self.sortedItemSets.count - 1 + if storyCount <= 0 { + titleText = "No Stories" + } else { + if storyCount == 1 { + titleText = "1 Story" + } else { + titleText = "\(storyCount) Stories" + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)), + environment: {}, + containerSize: CGSize(width: itemLayout.containerSize.width, height: 100.0) + ) + + let collapseStartIndex = 1 + + let collapsedItemWidth: CGFloat = 24.0 + let collapsedItemDistance: CGFloat = 14.0 + let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItemSets.count - collapseStartIndex, 3)) + var collapsedContentWidth: CGFloat = 0.0 + if collapsedItemCount > 0 { + collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0) + collapsedContentWidth += titleSpacing + } + + let collapseEndIndex = collapseStartIndex + Int(collapsedItemCount) + let _ = collapseEndIndex + + let titleOffset = collapsedContentWidth + collapsedContentWidth += titleSize.width + + let collapsedContentOrigin: CGFloat + let collapsedItemOffsetY: CGFloat + let itemScale: CGFloat + if hasStories { + collapsedContentOrigin = floor((itemLayout.containerSize.width - collapsedContentWidth) * 0.5) + itemScale = 1.0 + collapsedItemOffsetY = 0.0 + } else { + collapsedContentOrigin = itemLayout.frame(at: 0).minX + 30.0 + itemScale = 1.0//1.0 * (1.0 - component.collapseFraction) + 0.001 * component.collapseFraction + collapsedItemOffsetY = 16.0 + } + + let titleFrame = CGRect(origin: CGPoint(x: component.collapseFraction * (collapsedContentOrigin + titleOffset) + (1.0 - component.collapseFraction) * (itemLayout.containerSize.width), y: 19.0/* * component.collapseFraction + (1.0 - component.collapseFraction) * (-40.0)*/), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + self.scrollView.addSubview(titleView) + } + transition.setPosition(view: titleView, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY)) + transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size)) + + var titleAlpha: CGFloat = pow(component.collapseFraction, 1.5) + if !hasStories { + titleAlpha = 0.0 + } + transition.setAlpha(view: titleView, alpha: titleAlpha) + + transition.setScale(view: titleView, scale: (component.collapseFraction) * 1.0 + (1.0 - component.collapseFraction) * 0.001) + } + + let visibleBounds = self.scrollView.bounds + var validIds: [EnginePeer.Id] = [] for i in 0 ..< self.sortedItemSets.count { let itemSet = self.sortedItemSets[i] guard let peer = itemSet.peer else { continue } + + let regularItemFrame = itemLayout.frame(at: i) + if !visibleBounds.intersects(regularItemFrame) { + /*if keepVisibleUntilCompletion && self.visibleItems[itemSet.peerId] != nil { + } else {*/ + continue + //} + } + validIds.append(itemSet.peerId) let visibleItem: VisibleItem @@ -159,14 +285,53 @@ public final class StoryPeerListComponent: Component { } var hasUnseen = false - if peer.id != component.context.account.peerId { - for item in itemSet.items { - if !item.isSeen { - hasUnseen = true - } + let hasItems = !itemSet.items.isEmpty + var itemProgress: CGFloat? + if peer.id == component.context.account.peerId { + itemProgress = component.state?.uploadProgress + //itemProgress = 0.0 + } + + for item in itemSet.items { + if !item.isSeen { + hasUnseen = true } } + let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height)) + + let itemFrame = regularItemFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction) + + var leftItemFrame: CGRect? + var rightItemFrame: CGRect? + + var itemAlpha: CGFloat = 1.0 + + if i >= collapseStartIndex && i <= (collapseStartIndex + 2) { + 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) + } + if i != collapseStartIndex + 2 { + 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) + } + } else { + itemAlpha = pow(1.0 - component.collapseFraction, 1.5) + } + + var leftNeighborDistance: CGFloat? + var rightNeighborDistance: CGFloat? + + if let leftItemFrame { + leftNeighborDistance = abs(leftItemFrame.midX - itemFrame.midX) + } + if let rightItemFrame { + rightNeighborDistance = abs(rightItemFrame.midX - itemFrame.midX) + } + let _ = visibleItem.view.update( transition: itemTransition, component: AnyComponent(StoryPeerListItemComponent( @@ -175,19 +340,26 @@ public final class StoryPeerListComponent: Component { strings: component.strings, peer: peer, hasUnseen: hasUnseen, + hasItems: hasItems, + progress: itemProgress, + collapseFraction: component.collapseFraction, + collapsedWidth: collapsedItemWidth, + leftNeighborDistance: leftNeighborDistance, + rightNeighborDistance: rightNeighborDistance, action: component.peerAction )), environment: {}, containerSize: itemLayout.itemSize ) - let itemFrame = itemLayout.frame(at: i) - if let itemView = visibleItem.view.view { if itemView.superview == nil { self.scrollView.addSubview(itemView) } + itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01 itemTransition.setFrame(view: itemView, frame: itemFrame) + itemTransition.setAlpha(view: itemView, alpha: itemAlpha) + itemTransition.setScale(view: itemView, scale: itemScale) } } @@ -196,13 +368,21 @@ public final class StoryPeerListComponent: Component { if !validIds.contains(id) { removedIds.append(id) if let itemView = visibleItem.view.view { - itemView.removeFromSuperview() + if keepVisibleUntilCompletion && !transition.animation.isImmediate { + transition.attachAnimation(view: itemView, id: "keep", completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } } } } for id in removedIds { self.visibleItems.removeValue(forKey: id) } + + transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.containerSize.height - 8.0))) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -210,19 +390,41 @@ public final class StoryPeerListComponent: Component { } func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component != nil { + if component.collapseFraction != 0.0 && self.scrollView.bounds.minX != 0.0 { + self.ignoreScrolling = true + + let scrollingDistance = self.scrollView.bounds.minX + self.scrollView.bounds = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size) + let tempTransition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + self.updateScrolling(transition: tempTransition, keepVisibleUntilCompletion: true) + tempTransition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: scrollingDistance, y: 0.0), to: CGPoint(), additive: true) + + self.ignoreScrolling = false + } + } + self.component = component self.state = state + self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne + self.sortedItemSets.removeAll(keepingCapacity: true) if let state = component.state { if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) { self.sortedItemSets.append(state.itemSets[myIndex]) } - for itemSet in state.itemSets { - if itemSet.peerId == component.context.account.peerId { - continue + for i in 0 ..< 4 { + for itemSet in state.itemSets { + if itemSet.peerId == component.context.account.peerId { + continue + } + if i == 0 { + self.sortedItemSets.append(itemSet) + } else { + self.sortedItemSets.append(StoryListContext.PeerItemSet(peerId: EnginePeer.Id(namespace: itemSet.peerId.namespace, id: EnginePeer.Id.Id._internalFromInt64Value(itemSet.peerId.id._internalGetInt64Value() + Int64(i))), peer: itemSet.peer, items: itemSet.items, totalCount: itemSet.totalCount)) + } } - self.sortedItemSets.append(itemSet) } } @@ -243,7 +445,7 @@ public final class StoryPeerListComponent: Component { } self.ignoreScrolling = false - self.updateScrolling(transition: transition) + self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false) return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 54daba591d..7d6ceb3efd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -10,12 +10,137 @@ import SwiftSignalKit import TelegramPresentationData import AvatarNode +private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? { + let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y) + let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y) + if distance > radius * 2.0 || distance == 0.0 { + return nil + } + + let x1 = center.x + let y1 = center.y + let x2 = otherCenter.x + let y2 = otherCenter.y + let r1 = radius + let r2 = radius + let R = distance + + let ix1: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1) + let ix2: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1) + + let iy1: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2) + let iy2: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2) + + var v1 = CGPoint(x: ix1 - center.x, y: iy1 - center.y) + let length1 = sqrt(v1.x * v1.x + v1.y * v1.y) + v1.x /= length1 + v1.y /= length1 + + var v2 = CGPoint(x: ix2 - center.x, y: iy2 - center.y) + let length2 = sqrt(v2.x * v2.x + v2.y * v2.y) + v2.x /= length2 + v2.y /= length2 + + var point1Angle = atan(v1.y / v1.x) + var point2Angle = atan(v2.y / v2.x) + + if distanceVector.x < 0.0 { + point1Angle += CGFloat.pi + point2Angle += CGFloat.pi + } + + return (point1Angle, point2Angle) +} + +private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat) -> CGPath { + let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } + let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } + + let path = CGMutablePath() + + if let leftAngles, let rightAngles { + path.addArc(center: center, radius: radius, startAngle: leftAngles.point1Angle, endAngle: rightAngles.point2Angle, clockwise: true) + + path.move(to: CGPoint(x: center.x + cos(rightAngles.point1Angle) * radius, y: center.y + sin(rightAngles.point1Angle) * radius)) + path.addArc(center: center, radius: radius, startAngle: rightAngles.point1Angle, endAngle: leftAngles.point2Angle, clockwise: true) + } else if let angles = leftAngles ?? rightAngles { + path.addArc(center: center, radius: radius, startAngle: angles.point1Angle, endAngle: angles.point2Angle, clockwise: true) + } else { + path.addEllipse(in: CGRect(origin: CGPoint(x: center.x - radius, y: center.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } + + return path +} + +private final class StoryProgressLayer: SimpleShapeLayer { + private struct Params: Equatable { + var size: CGSize + var lineWidth: CGFloat + } + + private var currentParams: Params? + + override init() { + super.init() + + self.fillColor = UIColor.white.cgColor + self.fillRule = .evenOdd + + self.fillColor = nil + self.strokeColor = UIColor.white.cgColor + self.lineWidth = 2.0 + self.lineCap = .round + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, lineWidth: CGFloat) { + let params = Params( + size: size, + lineWidth: lineWidth + ) + if self.currentParams == params { + return + } + self.currentParams = params + + let lineWidth: CGFloat = 2.0 + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: size.width * 0.5 - lineWidth * 0.5, startAngle: 0.0, endAngle: CGFloat.pi * 0.25, clockwise: false) + + self.path = path + + if self.animation(forKey: "rotation") == nil { + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.duration = 2.0 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0)) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + self.add(basicAnimation, forKey: "rotation") + } + } +} + public final class StoryPeerListItemComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let peer: EnginePeer public let hasUnseen: Bool + public let hasItems: Bool + public let progress: CGFloat? + public let collapseFraction: CGFloat + public let collapsedWidth: CGFloat + public let leftNeighborDistance: CGFloat? + public let rightNeighborDistance: CGFloat? public let action: (EnginePeer) -> Void public init( @@ -24,6 +149,12 @@ public final class StoryPeerListItemComponent: Component { strings: PresentationStrings, peer: EnginePeer, hasUnseen: Bool, + hasItems: Bool, + progress: CGFloat?, + collapseFraction: CGFloat, + collapsedWidth: CGFloat, + leftNeighborDistance: CGFloat?, + rightNeighborDistance: CGFloat?, action: @escaping (EnginePeer) -> Void ) { self.context = context @@ -31,6 +162,12 @@ public final class StoryPeerListItemComponent: Component { self.strings = strings self.peer = peer self.hasUnseen = hasUnseen + self.hasItems = hasItems + self.progress = progress + self.collapseFraction = collapseFraction + self.collapsedWidth = collapsedWidth + self.leftNeighborDistance = leftNeighborDistance + self.rightNeighborDistance = rightNeighborDistance self.action = action } @@ -50,24 +187,70 @@ public final class StoryPeerListItemComponent: Component { if lhs.hasUnseen != rhs.hasUnseen { return false } + if lhs.hasItems != rhs.hasItems { + return false + } + if lhs.progress != rhs.progress { + return false + } + if lhs.collapseFraction != rhs.collapseFraction { + return false + } + if lhs.collapsedWidth != rhs.collapsedWidth { + return false + } + if lhs.leftNeighborDistance != rhs.leftNeighborDistance { + return false + } + if lhs.rightNeighborDistance != rhs.rightNeighborDistance { + return false + } return true } public final class View: HighlightTrackingButton { + private let avatarContainer: UIView private var avatarNode: AvatarNode? - private let indicatorCircleView: UIImageView + private var avatarAddBadgeView: UIImageView? + private let avatarShapeLayer: SimpleShapeLayer + private let indicatorMaskLayer: SimpleLayer + private let indicatorColorLayer: SimpleGradientLayer + private var progressLayer: StoryProgressLayer? + private let indicatorShapeLayer: SimpleShapeLayer private let title = ComponentView() private var component: StoryPeerListItemComponent? private weak var componentState: EmptyComponentState? public override init(frame: CGRect) { - self.indicatorCircleView = UIImageView() - self.indicatorCircleView.isUserInteractionEnabled = false + self.avatarContainer = UIView() + self.avatarContainer.isUserInteractionEnabled = false + + self.avatarShapeLayer = SimpleShapeLayer() + + self.indicatorColorLayer = SimpleGradientLayer() + self.indicatorColorLayer.type = .axial + self.indicatorColorLayer.startPoint = CGPoint(x: 0.5, y: 0.0) + self.indicatorColorLayer.endPoint = CGPoint(x: 0.5, y: 1.0) + + self.indicatorMaskLayer = SimpleLayer() + self.indicatorShapeLayer = SimpleShapeLayer() super.init(frame: frame) - self.addSubview(self.indicatorCircleView) + self.addSubview(self.avatarContainer) + + self.layer.addSublayer(self.indicatorColorLayer) + self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer) + self.indicatorColorLayer.mask = self.indicatorMaskLayer + + self.avatarShapeLayer.fillColor = UIColor.white.cgColor + self.avatarShapeLayer.fillRule = .evenOdd + + self.indicatorShapeLayer.fillColor = nil + self.indicatorShapeLayer.strokeColor = UIColor.white.cgColor + self.indicatorShapeLayer.lineWidth = 2.0 + self.indicatorShapeLayer.lineCap = .round self.highligthedChanged = { [weak self] highlighted in guard let self else { @@ -100,70 +283,159 @@ public final class StoryPeerListItemComponent: Component { } func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let hadUnseen = self.component?.hasUnseen ?? false + let hadUnseen = self.component?.hasUnseen + let hadProgress = self.component?.progress != nil + let themeUpdated = self.component?.theme !== component.theme self.component = component self.componentState = state + 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.collapseFraction + let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) self.avatarNode = avatarNode + avatarNode.layer.mask = self.avatarShapeLayer avatarNode.isUserInteractionEnabled = false - self.addSubview(avatarNode.view) + self.avatarContainer.addSubview(avatarNode.view) } let avatarSize = CGSize(width: 52.0, height: 52.0) - let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5), y: 4.0), size: avatarSize) + let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: 4.0), size: avatarSize) + + transition.setFrame(view: avatarNode.view, frame: CGRect(origin: CGPoint(), size: avatarFrame.size)) + let indicatorFrame = avatarFrame.insetBy(dx: -4.0, dy: -4.0) + let indicatorLineWidth: CGFloat = 2.0 * (1.0 - component.collapseFraction) + (1.33 * (1.0 / effectiveScale)) * (component.collapseFraction) + avatarNode.setPeer( context: component.context, theme: component.theme, peer: component.peer ) avatarNode.updateSize(size: avatarSize) - transition.setFrame(view: avatarNode.view, frame: avatarFrame) + transition.setPosition(view: self.avatarContainer, position: avatarFrame.center) + transition.setBounds(view: self.avatarContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - if component.peer.id == component.context.account.peerId && !component.hasUnseen { - self.indicatorCircleView.image = nil - } else if self.indicatorCircleView.image == nil || hadUnseen != component.hasUnseen { - self.indicatorCircleView.image = generateImage(indicatorFrame.size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - let lineWidth: CGFloat = 2.0 - context.setLineWidth(lineWidth) - context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) - context.replacePathWithStrokedPath() - context.clip() - - var locations: [CGFloat] = [1.0, 0.0] - let colors: [CGColor] - - if component.hasUnseen { - colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] - } else { - colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) + let scaledAvatarSize = effectiveScale * (avatarSize.width + 4.0 - indicatorLineWidth * 2.0) + + transition.setScale(view: self.avatarContainer, scale: scaledAvatarSize / avatarSize.width) + + if component.peer.id == component.context.account.peerId && !component.hasItems { + self.indicatorColorLayer.isHidden = true + + let avatarAddBadgeView: UIImageView + var avatarAddBadgeTransition = transition + if let current = self.avatarAddBadgeView { + avatarAddBadgeView = current + } else { + avatarAddBadgeTransition = .immediate + avatarAddBadgeView = UIImageView() + self.avatarAddBadgeView = avatarAddBadgeView + self.avatarContainer.addSubview(avatarAddBadgeView) + } + let badgeSize = CGSize(width: 16.0, height: 16.0) + if avatarAddBadgeView.image == nil || themeUpdated { + avatarAddBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(component.theme.list.itemCheckColors.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setStrokeColor(component.theme.list.itemCheckColors.foregroundColor.cgColor) + context.setLineWidth(UIScreenPixel * 3.0) + context.setLineCap(.round) + + let lineSize: CGFloat = 9.0 + UIScreenPixel + + context.move(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5)) + context.addLine(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5 + lineSize)) + context.strokePath() + + context.move(to: CGPoint(x: (size.width - lineSize) * 0.5, y: size.height * 0.5)) + context.addLine(to: CGPoint(x: (size.width - lineSize) * 0.5 + lineSize, y: size.height * 0.5)) + context.strokePath() + }) + } + avatarAddBadgeTransition.setFrame(view: avatarAddBadgeView, frame: CGRect(origin: CGPoint(x: avatarFrame.width - 1.0 - badgeSize.width, y: avatarFrame.height - 1.0 - badgeSize.height), size: badgeSize)) + } else { + if indicatorColorLayer.isHidden { + self.indicatorColorLayer.isHidden = false + } + + if let avatarAddBadgeView = self.avatarAddBadgeView { + self.avatarAddBadgeView = nil + avatarAddBadgeView.removeFromSuperview() + } } - transition.setFrame(view: self.indicatorCircleView, frame: indicatorFrame) + + if hadUnseen != component.hasUnseen || hadProgress != (component.progress != nil) { + let locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] + + if component.hasUnseen || component.progress != nil { + colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } + + self.indicatorColorLayer.locations = locations.map { $0 as NSNumber } + self.indicatorColorLayer.colors = colors + } + + transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.center) + transition.setBounds(layer: self.indicatorColorLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) + transition.setPosition(layer: self.indicatorShapeLayer, position: CGPoint(x: indicatorFrame.width * 0.5, y: indicatorFrame.height * 0.5)) + transition.setBounds(layer: self.indicatorShapeLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) + transition.setScale(layer: self.indicatorColorLayer, scale: effectiveScale) + + let indicatorCenter = CGRect(origin: CGPoint(), size: indicatorFrame.size).center + + var mappedLeftCenter: CGPoint? + var mappedRightCenter: CGPoint? + + if let leftNeighborDistance = component.leftNeighborDistance { + mappedLeftCenter = CGPoint(x: indicatorCenter.x - leftNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y) + } + if let rightNeighborDistance = component.rightNeighborDistance { + mappedRightCenter = CGPoint(x: indicatorCenter.x + rightNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y) + } + + let avatarPath = CGMutablePath() + avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -1.0, dy: -1.0)) + if component.peer.id == component.context.account.peerId && !component.hasItems { + let cutoutSize: CGFloat = 18.0 + UIScreenPixel * 2.0 + avatarPath.addEllipse(in: CGRect(origin: CGPoint(x: avatarSize.width - cutoutSize + UIScreenPixel, y: avatarSize.height - cutoutSize + UIScreenPixel), size: CGSize(width: cutoutSize, height: cutoutSize))) + } else if let mappedRightCenter { + avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineWidth, dy: -indicatorLineWidth).offsetBy(dx: abs(mappedRightCenter.x - indicatorCenter.x), dy: 0.0)) + } + self.avatarShapeLayer.path = avatarPath + + self.indicatorShapeLayer.path = calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorFrame.width * 0.5 - indicatorLineWidth * 0.5) //TODO:localize + let titleString: String + if component.peer.id == component.context.account.peerId { + if let _ = component.progress { + titleString = "Uploading..." + } else { + titleString = "My story" + } + } else { + titleString = component.peer.compactDisplayTitle + } let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: component.peer.id == component.context.account.peerId ? "My story" : component.peer.compactDisplayTitle, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)), + component: AnyComponent(Text(text: titleString, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: indicatorFrame.maxY + 3.0), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.25, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 3.0) * effectiveScale), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() @@ -171,7 +443,40 @@ public final class StoryPeerListItemComponent: Component { self.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) - transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size)) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setScale(view: titleView, scale: effectiveScale) + transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction) + } + + if component.progress != nil { + var progressTransition = transition + let progressLayer: StoryProgressLayer + if let current = self.progressLayer { + progressLayer = current + } else { + progressTransition = .immediate + progressLayer = StoryProgressLayer() + self.progressLayer = progressLayer + self.indicatorMaskLayer.addSublayer(progressLayer) + } + let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size) + progressTransition.setFrame(layer: progressLayer, frame: progressFrame) + progressLayer.update(size: progressFrame.size, lineWidth: 4.0) + + self.indicatorShapeLayer.opacity = 0.0 + } else { + self.indicatorShapeLayer.opacity = 1.0 + + if let progressLayer = self.progressLayer { + self.progressLayer = nil + if transition.animation.isImmediate { + progressLayer.removeFromSuperlayer() + } else { + progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in + progressLayer?.removeFromSuperlayer() + }) + } + } } return availableSize diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index e954cfeef5..a858ecbce0 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -22,6 +22,7 @@ import LegacyComponents import LegacyMediaPickerUI import LegacyCamera import AvatarNode +import LocalMediaResources private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -61,7 +62,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh } } -public final class TelegramRootController: NavigationController { +public final class TelegramRootController: NavigationController, TelegramRootControllerInterface { private let context: AccountContext public var rootTabController: TabBarController? @@ -270,7 +271,7 @@ public final class TelegramRootController: NavigationController { item = TGMediaAsset(phAsset: asset) } let context = self.context - legacyStoryMediaEditor(context: self.context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in + legacyStoryMediaEditor(context: context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in dismissCameraImpl?() guard let self else { @@ -362,20 +363,49 @@ public final class TelegramRootController: NavigationController { let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true - PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in - if let data, let image = UIImage(data: data) { - Queue.mainQueue().async { - let _ = (context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy) - |> deliverOnMainQueue).start(completed: { + switch asset.mediaType { + case .image: + PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in + if let data, let image = UIImage(data: data) { + Queue.mainQueue().async { guard let self else { return } - let _ = self + if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { + storyListContext.upload(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy) + } selectionController?.dismiss() - }) + + /*let _ = (context.engine.messages.uploadStory(media: ) + |> deliverOnMainQueue).start(completed: { + guard let self else { + return + } + let _ = self + selectionController?.dismiss() + })*/ + } } + }) + case .video: + let resource = VideoLibraryMediaResource(localIdentifier: asset.localIdentifier, conversion: VideoLibraryMediaResourceConversion.passthrough) + + if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { + storyListContext.upload(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), privacy: privacy) } - }) + selectionController?.dismiss() + + /*let _ = (context.engine.messages.uploadStory(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), privacy: privacy) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + let _ = self + selectionController?.dismiss() + })*/ + default: + selectionController?.dismiss() + } } }) }, present: { c, a in