mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
[WIP] Stories
This commit is contained in:
parent
e8dab90584
commit
8e28d85626
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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<Empty>?
|
||||
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<Empty>
|
||||
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
|
||||
|
@ -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?
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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<PendingMessageUploadedContentResult, PendingMessageUploadError>
|
||||
switch contentToUpload {
|
||||
case let .immediate(result, _):
|
||||
contentSignal = .single(result)
|
||||
case let .signal(signal, _):
|
||||
contentSignal = signal
|
||||
}
|
||||
|
||||
return contentSignal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return account.postbox.transaction { transaction -> Signal<Never, NoError> 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<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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<Empty>?
|
||||
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<Empty>?
|
||||
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<Empty>
|
||||
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<Empty>, 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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<Empty>()
|
||||
|
||||
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<Empty>, 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
|
||||
}
|
||||
|
@ -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<Empty>()
|
||||
|
||||
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<Empty>, 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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user