mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45: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 AttachmentFileController: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol TelegramRootControllerInterface: NavigationController {
|
||||||
|
func openStoryCamera()
|
||||||
|
}
|
||||||
|
|
||||||
public protocol SharedAccountContext: AnyObject {
|
public protocol SharedAccountContext: AnyObject {
|
||||||
var sharedContainerPath: String { get }
|
var sharedContainerPath: String { get }
|
||||||
var basePath: 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)
|
case chat(PeerId)
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case let .chat(peerId):
|
||||||
|
return "chat:\(peerId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FetchManagerForegroundDirection {
|
public enum FetchManagerForegroundDirection {
|
||||||
|
@ -47,12 +47,46 @@ import InviteLinksUI
|
|||||||
import ChatFolderLinkPreviewScreen
|
import ChatFolderLinkPreviewScreen
|
||||||
import StoryContainerScreen
|
import StoryContainerScreen
|
||||||
import StoryContentComponent
|
import StoryContentComponent
|
||||||
import StoryPeerListComponent
|
|
||||||
|
|
||||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||||
if listNode.scroller.isDragging {
|
if listNode.scroller.isDragging {
|
||||||
return false
|
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 {
|
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
|
||||||
let offset: CGFloat
|
let offset: CGFloat
|
||||||
if searchNode.expansionProgress < 0.6 {
|
if searchNode.expansionProgress < 0.6 {
|
||||||
@ -185,7 +219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
private var searchContentNode: NavigationBarSearchContentNode?
|
private var searchContentNode: NavigationBarSearchContentNode?
|
||||||
|
|
||||||
private let navigationSecondaryContentNode: ASDisplayNode
|
private let navigationSecondaryContentNode: ASDisplayNode
|
||||||
private var storyPeerListView: ComponentView<Empty>?
|
|
||||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
|
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
|
||||||
|
|
||||||
@ -210,7 +243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
|
|
||||||
private var powerSavingMonitoringDisposable: Disposable?
|
private var powerSavingMonitoringDisposable: Disposable?
|
||||||
|
|
||||||
private var storyListContext: StoryListContext?
|
public private(set) var storyListContext: StoryListContext?
|
||||||
private var storyListState: StoryListContext.State?
|
private var storyListState: StoryListContext.State?
|
||||||
private var storyListStateDisposable: Disposable?
|
private var storyListStateDisposable: Disposable?
|
||||||
|
|
||||||
@ -444,7 +477,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
tabsIsEmpty = true
|
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 {
|
enum State: Equatable {
|
||||||
case empty(hasDownloads: Bool)
|
case empty(hasDownloads: Bool)
|
||||||
@ -2123,12 +2159,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
tabsIsEmpty = true
|
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 {
|
if wasEmpty != isEmpty {
|
||||||
self.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||||
} else if let componentView = self.storyPeerListView?.view, !componentView.bounds.isEmpty {
|
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
|
||||||
self.updateStoryPeerListView(size: componentView.bounds.size, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
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)
|
super.updateNavigationBarLayout(layout, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
private func updateHeaderStories(transition: ContainedViewLayoutTransition) {
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
|
||||||
|
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||||
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
|
componentView.storyPeerAction = { [weak self] peer in
|
||||||
|
|
||||||
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
|
|
||||||
guard let self, let storyListContext = self.storyListContext else {
|
guard let self, let storyListContext = self.storyListContext else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2401,8 +2420,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||||
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
|
if let peer, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||||
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peer.id) {
|
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
|
||||||
transitionIn = StoryContainerScreen.TransitionIn(
|
transitionIn = StoryContainerScreen.TransitionIn(
|
||||||
sourceView: transitionView,
|
sourceView: transitionView,
|
||||||
sourceRect: transitionView.bounds,
|
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(
|
let storyContainerScreen = StoryContainerScreen(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
initialFocusedId: AnyHashable(peer.id),
|
initialFocusedId: initialFocusedId,
|
||||||
initialContent: initialContent,
|
initialContent: initialContent,
|
||||||
transitionIn: transitionIn,
|
transitionIn: transitionIn,
|
||||||
transitionOut: { [weak self] peerId in
|
transitionOut: { [weak self] peerId in
|
||||||
@ -2421,8 +2455,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
|
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||||
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peerId) {
|
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||||
return StoryContainerScreen.TransitionOut(
|
return StoryContainerScreen.TransitionOut(
|
||||||
destinationView: transitionView,
|
destinationView: transitionView,
|
||||||
destinationRect: transitionView.bounds,
|
destinationRect: transitionView.bounds,
|
||||||
@ -2437,10 +2471,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
self.push(storyContainerScreen)
|
self.push(storyContainerScreen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)),
|
|
||||||
environment: {},
|
let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
|
||||||
containerSize: size
|
|
||||||
)
|
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) {
|
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)))
|
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 {
|
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))
|
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
|
strongSelf.didSetupTabs = true
|
||||||
|
|
||||||
if strongSelf.displayNavigationBar {
|
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)
|
strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3309,6 +3340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
||||||
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
||||||
}
|
}
|
||||||
|
strongSelf.searchContentNode?.additionalHeight = 0.0
|
||||||
|
|
||||||
activate(filter != .downloads)
|
activate(filter != .downloads)
|
||||||
|
|
||||||
@ -3362,7 +3394,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
filterContainerNode = searchContentNode.filterContainerNode
|
filterContainerNode = searchContentNode.filterContainerNode
|
||||||
|
|
||||||
if let filterContainerNode = filterContainerNode, let snapshotView = filterContainerNode.view.snapshotView(afterScreenUpdates: false) {
|
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)
|
filterContainerNode.view.superview?.addSubview(snapshotView)
|
||||||
|
|
||||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
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 {
|
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)
|
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)
|
self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false)
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
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)
|
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()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.listNode)
|
self.addSubnode(self.listNode)
|
||||||
@ -1481,7 +1485,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
|
private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
|
||||||
private(set) var inlineStackContainerNode: ChatListContainerNode?
|
private(set) var inlineStackContainerNode: ChatListContainerNode?
|
||||||
private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
|
private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
|
||||||
private(set) var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
|
var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
|
||||||
|
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
var navigationBar: NavigationBar?
|
var navigationBar: NavigationBar?
|
||||||
|
@ -1184,6 +1184,12 @@ public final class ChatListNode: ListView {
|
|||||||
private var pollFilterUpdatesDisposable: Disposable?
|
private var pollFilterUpdatesDisposable: Disposable?
|
||||||
private var chatFilterUpdatesDisposable: 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) {
|
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.context = context
|
||||||
self.location = location
|
self.location = location
|
||||||
@ -1204,12 +1210,14 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
|
self.scrollHeightTopInset = navigationBarSearchContentHeight
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
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
|
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
|
||||||
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
|
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:
|
case let .known(value) where abs(value) < .ulpOfOne:
|
||||||
offset = 0.0
|
offset = 0.0
|
||||||
default:
|
default:
|
||||||
offset = -navigationBarSearchContentHeight
|
offset = -self.scrollHeightTopInset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
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 {
|
var isNavigationHidden: Bool {
|
||||||
switch self.visibleContentOffset() {
|
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
|
return false
|
||||||
case .none:
|
case .none:
|
||||||
return false
|
return false
|
||||||
@ -3165,11 +3173,11 @@ public final class ChatListNode: ListView {
|
|||||||
var isNavigationInAFinalState: Bool {
|
var isNavigationInAFinalState: Bool {
|
||||||
switch self.visibleContentOffset() {
|
switch self.visibleContentOffset() {
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
if value < navigationBarSearchContentHeight - 1.0 {
|
if value < self.scrollHeightTopInset - 1.0 {
|
||||||
if abs(value - 0.0) < 1.0 {
|
if abs(value - 0.0) < 1.0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if abs(value - navigationBarSearchContentHeight) < 1.0 {
|
if abs(value - self.scrollHeightTopInset) < 1.0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -3187,9 +3195,9 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
var scrollToItem: ListViewScrollToItem?
|
var scrollToItem: ListViewScrollToItem?
|
||||||
switch self.visibleContentOffset() {
|
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 {
|
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:
|
default:
|
||||||
if !isNavigationHidden {
|
if !isNavigationHidden {
|
||||||
|
@ -1395,7 +1395,6 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
|
|
||||||
if let customHeaderContentView = self.customHeaderContentView {
|
if let customHeaderContentView = self.customHeaderContentView {
|
||||||
let headerSize = CGSize(width: size.width, height: nominalHeight)
|
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))
|
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) {
|
if let result = self.additionalContentNode.view.hitTest(self.view.convert(point, to: self.additionalContentNode.view), with: event) {
|
||||||
return result
|
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 {
|
guard let result = super.hitTest(point, with: event) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) {
|
if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,9 @@ private final class FetchManagerCategoryContext {
|
|||||||
previousPriorityKey = nil
|
previousPriorityKey = nil
|
||||||
let (mediaReference, resourceReference, statsCategory, episode) = takeNew()
|
let (mediaReference, resourceReference, statsCategory, episode) = takeNew()
|
||||||
entry = FetchManagerLocationEntry(id: id, episode: episode, mediaReference: mediaReference, resourceReference: resourceReference, statsCategory: statsCategory)
|
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
|
self.entries[id] = entry
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
@ -236,7 +239,7 @@ private final class FetchManagerCategoryContext {
|
|||||||
}
|
}
|
||||||
activeContext.disposable?.dispose()
|
activeContext.disposable?.dispose()
|
||||||
let postbox = self.postbox
|
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
|
var userLocation: MediaResourceUserLocation = .other
|
||||||
switch entry.id.location {
|
switch entry.id.location {
|
||||||
@ -401,7 +404,7 @@ private final class FetchManagerCategoryContext {
|
|||||||
} else if ranges.isEmpty {
|
} else if ranges.isEmpty {
|
||||||
} else {
|
} else {
|
||||||
let postbox = self.postbox
|
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
|
var userLocation: MediaResourceUserLocation = .other
|
||||||
switch entry.id.location {
|
switch entry.id.location {
|
||||||
|
@ -19,6 +19,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
|
|||||||
private var disabledOverlay: ASDisplayNode?
|
private var disabledOverlay: ASDisplayNode?
|
||||||
|
|
||||||
public private(set) var expansionProgress: CGFloat = 1.0
|
public private(set) var expansionProgress: CGFloat = 1.0
|
||||||
|
|
||||||
|
public var additionalHeight: CGFloat = 0.0
|
||||||
|
|
||||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
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)
|
let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, textColor, fillColor, backgroundColor, transition)
|
||||||
searchBarApply()
|
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)
|
transition.updateFrame(node: self.placeholderNode, frame: searchBarFrame)
|
||||||
|
|
||||||
self.placeholderHeight = searchBarHeight
|
self.placeholderHeight = searchBarHeight
|
||||||
@ -151,7 +154,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public var nominalHeight: CGFloat {
|
override public var nominalHeight: CGFloat {
|
||||||
return navigationBarSearchContentHeight
|
return navigationBarSearchContentHeight + self.additionalHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
override public var mode: NavigationBarContentMode {
|
override public var mode: NavigationBarContentMode {
|
||||||
|
@ -5,6 +5,7 @@ import TelegramApi
|
|||||||
|
|
||||||
public enum EngineStoryInputMedia {
|
public enum EngineStoryInputMedia {
|
||||||
case image(dimensions: PixelDimensions, data: Data)
|
case image(dimensions: PixelDimensions, data: Data)
|
||||||
|
case video(dimensions: PixelDimensions, duration: Int, resource: TelegramMediaResource)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct EngineStoryPrivacy: Equatable {
|
public struct EngineStoryPrivacy: Equatable {
|
||||||
@ -141,6 +142,124 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, priva
|
|||||||
}
|
}
|
||||||
|> switchToLatest
|
|> 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 struct State: Equatable {
|
||||||
public var itemSets: [PeerItemSet]
|
public var itemSets: [PeerItemSet]
|
||||||
|
public var uploadProgress: CGFloat?
|
||||||
public var loadMoreToken: LoadMoreToken?
|
public var loadMoreToken: LoadMoreToken?
|
||||||
|
|
||||||
public init(itemSets: [PeerItemSet], loadMoreToken: LoadMoreToken?) {
|
public init(itemSets: [PeerItemSet], uploadProgress: CGFloat?, loadMoreToken: LoadMoreToken?) {
|
||||||
self.itemSets = itemSets
|
self.itemSets = itemSets
|
||||||
|
self.uploadProgress = uploadProgress
|
||||||
self.loadMoreToken = loadMoreToken
|
self.loadMoreToken = loadMoreToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class UploadContext {
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class Impl {
|
private final class Impl {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let account: Account
|
private let account: Account
|
||||||
@ -127,6 +136,12 @@ public final class StoryListContext {
|
|||||||
private var updatesDisposable: Disposable?
|
private var updatesDisposable: Disposable?
|
||||||
private var peerDisposables: [PeerId: Disposable] = [:]
|
private var peerDisposables: [PeerId: Disposable] = [:]
|
||||||
|
|
||||||
|
private var uploadContexts: [UploadContext] = [] {
|
||||||
|
didSet {
|
||||||
|
self.stateValue.uploadProgress = self.uploadContexts.isEmpty ? nil : 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var stateValue: State {
|
private var stateValue: State {
|
||||||
didSet {
|
didSet {
|
||||||
self.state.set(.single(self.stateValue))
|
self.state.set(.single(self.stateValue))
|
||||||
@ -139,9 +154,21 @@ public final class StoryListContext {
|
|||||||
self.account = account
|
self.account = account
|
||||||
self.scope = scope
|
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))
|
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
|
self.updatesDisposable = (account.stateManager.storyUpdates
|
||||||
|> deliverOn(queue)).start(next: { [weak self] updates in
|
|> deliverOn(queue)).start(next: { [weak self] updates in
|
||||||
if updates.isEmpty {
|
if updates.isEmpty {
|
||||||
@ -150,6 +177,11 @@ public final class StoryListContext {
|
|||||||
|
|
||||||
let _ = account.postbox.transaction({ transaction -> [PeerId: Peer] in
|
let _ = account.postbox.transaction({ transaction -> [PeerId: Peer] in
|
||||||
var peers: [PeerId: Peer] = [:]
|
var peers: [PeerId: Peer] = [:]
|
||||||
|
|
||||||
|
if let peer = transaction.getPeer(account.peerId) {
|
||||||
|
peers[peer.id] = peer
|
||||||
|
}
|
||||||
|
|
||||||
for update in updates {
|
for update in updates {
|
||||||
switch update {
|
switch update {
|
||||||
case let .added(peerId, _):
|
case let .added(peerId, _):
|
||||||
@ -198,7 +230,21 @@ public final class StoryListContext {
|
|||||||
if let index = items.firstIndex(where: { $0.id == item.id }) {
|
if let index = items.firstIndex(where: { $0.id == item.id }) {
|
||||||
items.remove(at: index)
|
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
|
items.sort(by: { lhsItem, rhsItem in
|
||||||
if lhsItem.timestamp != rhsItem.timestamp {
|
if lhsItem.timestamp != rhsItem.timestamp {
|
||||||
return lhsItem.timestamp < rhsItem.timestamp
|
return lhsItem.timestamp < rhsItem.timestamp
|
||||||
@ -263,6 +309,12 @@ public final class StoryListContext {
|
|||||||
return lhsItem.id > rhsItem.id
|
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
|
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) {
|
func loadMore(refresh: Bool) {
|
||||||
if self.isLoadingMore {
|
if self.isLoadingMore {
|
||||||
return
|
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) })
|
return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -589,7 +662,7 @@ public final class StoryListContext {
|
|||||||
return lhsItem.id > rhsItem.id
|
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)
|
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/TelegramPresentationData",
|
||||||
"//submodules/TelegramUI/Components/ChatListTitleView",
|
"//submodules/TelegramUI/Components/ChatListTitleView",
|
||||||
"//submodules/AccountContext",
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
"//submodules/AsyncDisplayKit",
|
"//submodules/AsyncDisplayKit",
|
||||||
"//submodules/AnimationUI",
|
"//submodules/AnimationUI",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -6,6 +6,8 @@ import TelegramPresentationData
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ChatListTitleView
|
import ChatListTitleView
|
||||||
import AppBundle
|
import AppBundle
|
||||||
|
import StoryPeerListComponent
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
public final class HeaderNetworkStatusComponent: Component {
|
public final class HeaderNetworkStatusComponent: Component {
|
||||||
public enum Content: Equatable {
|
public enum Content: Equatable {
|
||||||
@ -272,10 +274,13 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
var backButtonView: BackButtonView?
|
var backButtonView: BackButtonView?
|
||||||
|
|
||||||
let titleOffsetContainer: UIView
|
let titleOffsetContainer: UIView
|
||||||
|
let titleScaleContainer: UIView
|
||||||
let titleTextView: ImmediateTextView
|
let titleTextView: ImmediateTextView
|
||||||
var titleContentView: ComponentView<Empty>?
|
var titleContentView: ComponentView<Empty>?
|
||||||
var chatListTitleView: ChatListTitleView?
|
var chatListTitleView: ChatListTitleView?
|
||||||
|
|
||||||
|
var contentOffsetFraction: CGFloat = 0.0
|
||||||
|
|
||||||
init(
|
init(
|
||||||
backPressed: @escaping () -> Void,
|
backPressed: @escaping () -> Void,
|
||||||
openStatusSetup: @escaping (UIView) -> Void,
|
openStatusSetup: @escaping (UIView) -> Void,
|
||||||
@ -288,16 +293,18 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
self.leftButtonOffsetContainer = UIView()
|
self.leftButtonOffsetContainer = UIView()
|
||||||
self.rightButtonOffsetContainer = UIView()
|
self.rightButtonOffsetContainer = UIView()
|
||||||
self.titleOffsetContainer = UIView()
|
self.titleOffsetContainer = UIView()
|
||||||
|
self.titleScaleContainer = UIView()
|
||||||
|
|
||||||
self.titleTextView = ImmediateTextView()
|
self.titleTextView = ImmediateTextView()
|
||||||
|
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
self.addSubview(self.titleOffsetContainer)
|
self.addSubview(self.titleOffsetContainer)
|
||||||
|
self.titleOffsetContainer.addSubview(self.titleScaleContainer)
|
||||||
self.addSubview(self.leftButtonOffsetContainer)
|
self.addSubview(self.leftButtonOffsetContainer)
|
||||||
self.addSubview(self.rightButtonOffsetContainer)
|
self.addSubview(self.rightButtonOffsetContainer)
|
||||||
|
|
||||||
self.titleOffsetContainer.addSubview(self.titleTextView)
|
self.titleScaleContainer.addSubview(self.titleTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -329,10 +336,28 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
return nil
|
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) {
|
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
|
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()
|
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))
|
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
|
||||||
|
|
||||||
if let backButtonView = self.backButtonView {
|
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
|
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()
|
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))
|
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 {
|
if let backButtonView = self.backButtonView {
|
||||||
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
|
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) {
|
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)
|
self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||||
|
|
||||||
let buttonSpacing: CGFloat = 8.0
|
let buttonSpacing: CGFloat = 8.0
|
||||||
@ -530,7 +594,7 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
)
|
)
|
||||||
if let titleContentComponentView = titleContentView.view {
|
if let titleContentComponentView = titleContentView.view {
|
||||||
if titleContentComponentView.superview == nil {
|
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))
|
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 = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer)
|
||||||
chatListTitleView.manualLayout = true
|
chatListTitleView.manualLayout = true
|
||||||
self.chatListTitleView = chatListTitleView
|
self.chatListTitleView = chatListTitleView
|
||||||
self.titleOffsetContainer.addSubview(chatListTitleView)
|
self.titleScaleContainer.addSubview(chatListTitleView)
|
||||||
}
|
}
|
||||||
|
|
||||||
let chatListTitleContentSize = size
|
let chatListTitleContentSize = size
|
||||||
@ -591,6 +655,10 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
|
|
||||||
private var primaryContentView: ContentView?
|
private var primaryContentView: ContentView?
|
||||||
private var secondaryContentView: 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? {
|
private var effectiveContentView: ContentView? {
|
||||||
return self.secondaryContentView ?? self.primaryContentView
|
return self.secondaryContentView ?? self.primaryContentView
|
||||||
@ -598,6 +666,8 @@ public final class ChatListHeaderComponent: Component {
|
|||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.storyOffsetFraction = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder: NSCoder) {
|
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 {
|
func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
self.state = state
|
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)
|
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))
|
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 {
|
} else if let primaryContentView = self.primaryContentView {
|
||||||
self.primaryContentView = nil
|
self.primaryContentView = nil
|
||||||
primaryContentView.removeFromSuperview()
|
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)
|
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))
|
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 primaryContentView = self.primaryContentView {
|
||||||
if let previousComponent = previousComponent, previousComponent.secondaryContent == nil {
|
if let previousComponent = previousComponent, previousComponent.secondaryContent == nil {
|
||||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
if self.storyOffsetFraction < 0.8 {
|
||||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
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: {})
|
if self.storyOffsetFraction < 0.8 {
|
||||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
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 {
|
} else if let secondaryContentView = self.secondaryContentView {
|
||||||
self.secondaryContentView = nil
|
self.secondaryContentView = nil
|
||||||
|
|
||||||
if let primaryContentView = self.primaryContentView {
|
if let primaryContentView = self.primaryContentView {
|
||||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
|
if self.storyOffsetFraction < 0.8 {
|
||||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
|
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
|
||||||
secondaryContentView?.removeFromSuperview()
|
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 {
|
} else {
|
||||||
secondaryContentView.removeFromSuperview()
|
secondaryContentView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
@ -687,10 +687,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
self.currentSliceDisposable?.dispose()
|
self.currentSliceDisposable?.dispose()
|
||||||
if let focusedItemId = self.focusedItemId {
|
if let focusedItemId = self.focusedItemId {
|
||||||
if let item = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
|
|
||||||
item.markAsSeen?()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.currentSliceDisposable = (component.initialItemSlice.update(
|
self.currentSliceDisposable = (component.initialItemSlice.update(
|
||||||
component.initialItemSlice,
|
component.initialItemSlice,
|
||||||
focusedItemId
|
focusedItemId
|
||||||
|
@ -24,6 +24,7 @@ public enum StoryChatContent {
|
|||||||
position: items.count,
|
position: items.count,
|
||||||
component: AnyComponent(StoryItemContentComponent(
|
component: AnyComponent(StoryItemContentComponent(
|
||||||
context: context,
|
context: context,
|
||||||
|
peerId: peerId,
|
||||||
item: item
|
item: item
|
||||||
)),
|
)),
|
||||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||||
@ -57,7 +58,7 @@ public enum StoryChatContent {
|
|||||||
var sliceFocusedItemId: AnyHashable?
|
var sliceFocusedItemId: AnyHashable?
|
||||||
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) {
|
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) {
|
||||||
sliceFocusedItemId = AnyHashable(focusItem)
|
sliceFocusedItemId = AnyHashable(focusItem)
|
||||||
} else if itemSet.peerId != context.account.peerId {
|
} else {
|
||||||
if let id = itemSet.items.first(where: { !$0.isSeen })?.id {
|
if let id = itemSet.items.first(where: { !$0.isSeen })?.id {
|
||||||
sliceFocusedItemId = AnyHashable(id)
|
sliceFocusedItemId = AnyHashable(id)
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,12 @@ final class StoryItemContentComponent: Component {
|
|||||||
typealias EnvironmentType = StoryContentItem.Environment
|
typealias EnvironmentType = StoryContentItem.Environment
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
|
let peerId: EnginePeer.Id
|
||||||
let item: StoryListContext.Item
|
let item: StoryListContext.Item
|
||||||
|
|
||||||
init(context: AccountContext, item: StoryListContext.Item) {
|
init(context: AccountContext, peerId: EnginePeer.Id, item: StoryListContext.Item) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.peerId = peerId
|
||||||
self.item = item
|
self.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +29,9 @@ final class StoryItemContentComponent: Component {
|
|||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.peerId != rhs.peerId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.item != rhs.item {
|
if lhs.item != rhs.item {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -99,6 +104,9 @@ final class StoryItemContentComponent: Component {
|
|||||||
private var currentProgressTimerValue: Double = 0.0
|
private var currentProgressTimerValue: Double = 0.0
|
||||||
private var videoProgressDisposable: Disposable?
|
private var videoProgressDisposable: Disposable?
|
||||||
|
|
||||||
|
private var markedAsSeen: Bool = false
|
||||||
|
private var contentLoaded: Bool = false
|
||||||
|
|
||||||
private var videoPlaybackStatus: MediaPlayerStatus?
|
private var videoPlaybackStatus: MediaPlayerStatus?
|
||||||
|
|
||||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||||
@ -186,7 +194,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
|
|
||||||
private func updateIsProgressPaused() {
|
private func updateIsProgressPaused() {
|
||||||
if let videoNode = self.videoNode {
|
if let videoNode = self.videoNode {
|
||||||
if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy {
|
if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy {
|
||||||
videoNode.play()
|
videoNode.play()
|
||||||
} else {
|
} else {
|
||||||
videoNode.pause()
|
videoNode.pause()
|
||||||
@ -198,7 +206,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateProgressTimer() {
|
private func updateProgressTimer() {
|
||||||
let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy
|
let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||||
|
|
||||||
if needsTimer {
|
if needsTimer {
|
||||||
if self.currentProgressTimer == nil {
|
if self.currentProgressTimer == nil {
|
||||||
@ -206,13 +214,20 @@ final class StoryItemContentComponent: Component {
|
|||||||
timeout: 1.0 / 60.0,
|
timeout: 1.0 / 60.0,
|
||||||
repeat: true,
|
repeat: true,
|
||||||
completion: { [weak self] in
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.videoNode != nil {
|
if case .file = self.currentMessageMedia {
|
||||||
self.updateVideoPlaybackProgress()
|
self.updateVideoPlaybackProgress()
|
||||||
} else {
|
} 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
|
#if DEBUG && false
|
||||||
let currentProgressTimerLimit: Double = 5 * 60.0
|
let currentProgressTimerLimit: Double = 5 * 60.0
|
||||||
#else
|
#else
|
||||||
@ -273,6 +288,15 @@ final class StoryItemContentComponent: Component {
|
|||||||
progress = min(1.0, progress)
|
progress = min(1.0, progress)
|
||||||
|
|
||||||
currentProgress = 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))
|
let clippedProgress = max(0.0, min(1.0, currentProgress))
|
||||||
@ -325,6 +349,8 @@ final class StoryItemContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .file(file):
|
case let .file(file):
|
||||||
|
self.contentLoaded = true
|
||||||
|
|
||||||
signal = chatMessageVideo(
|
signal = chatMessageVideo(
|
||||||
postbox: component.context.account.postbox,
|
postbox: component.context.account.postbox,
|
||||||
userLocation: .other,
|
userLocation: .other,
|
||||||
@ -362,7 +388,15 @@ final class StoryItemContentComponent: Component {
|
|||||||
self.fetchDisposable?.dispose()
|
self.fetchDisposable?.dispose()
|
||||||
self.fetchDisposable = nil
|
self.fetchDisposable = nil
|
||||||
if let fetchSignal {
|
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
|
return availableSize
|
||||||
}
|
}
|
||||||
|
@ -14,19 +14,22 @@ public final class StoryPeerListComponent: Component {
|
|||||||
public let theme: PresentationTheme
|
public let theme: PresentationTheme
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
public let state: StoryListContext.State?
|
public let state: StoryListContext.State?
|
||||||
public let peerAction: (EnginePeer) -> Void
|
public let collapseFraction: CGFloat
|
||||||
|
public let peerAction: (EnginePeer?) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
state: StoryListContext.State?,
|
state: StoryListContext.State?,
|
||||||
peerAction: @escaping (EnginePeer) -> Void
|
collapseFraction: CGFloat,
|
||||||
|
peerAction: @escaping (EnginePeer?) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.collapseFraction = collapseFraction
|
||||||
self.peerAction = peerAction
|
self.peerAction = peerAction
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,9 @@ public final class StoryPeerListComponent: Component {
|
|||||||
if lhs.state != rhs.state {
|
if lhs.state != rhs.state {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.collapseFraction != rhs.collapseFraction {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +96,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class View: UIView, UIScrollViewDelegate {
|
public final class View: UIView, UIScrollViewDelegate {
|
||||||
|
private let collapsedButton: HighlightableButton
|
||||||
private let scrollView: ScrollView
|
private let scrollView: ScrollView
|
||||||
|
|
||||||
private var ignoreScrolling: Bool = false
|
private var ignoreScrolling: Bool = false
|
||||||
@ -98,10 +105,14 @@ public final class StoryPeerListComponent: Component {
|
|||||||
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
|
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
|
||||||
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
|
||||||
private var component: StoryPeerListComponent?
|
private var component: StoryPeerListComponent?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
|
self.collapsedButton = HighlightableButton()
|
||||||
|
|
||||||
self.scrollView = ScrollView()
|
self.scrollView = ScrollView()
|
||||||
self.scrollView.delaysContentTouches = false
|
self.scrollView.delaysContentTouches = false
|
||||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||||
@ -116,13 +127,43 @@ public final class StoryPeerListComponent: Component {
|
|||||||
|
|
||||||
self.scrollView.delegate = self
|
self.scrollView.delegate = self
|
||||||
self.addSubview(self.scrollView)
|
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) {
|
required public init?(coder: NSCoder) {
|
||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func collapsedButtonPressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.peerAction(nil)
|
||||||
|
}
|
||||||
|
|
||||||
public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? {
|
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 {
|
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||||
return itemView.transitionView()
|
return itemView.transitionView()
|
||||||
}
|
}
|
||||||
@ -131,21 +172,106 @@ public final class StoryPeerListComponent: Component {
|
|||||||
|
|
||||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
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 {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
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] = []
|
var validIds: [EnginePeer.Id] = []
|
||||||
for i in 0 ..< self.sortedItemSets.count {
|
for i in 0 ..< self.sortedItemSets.count {
|
||||||
let itemSet = self.sortedItemSets[i]
|
let itemSet = self.sortedItemSets[i]
|
||||||
guard let peer = itemSet.peer else {
|
guard let peer = itemSet.peer else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let regularItemFrame = itemLayout.frame(at: i)
|
||||||
|
if !visibleBounds.intersects(regularItemFrame) {
|
||||||
|
/*if keepVisibleUntilCompletion && self.visibleItems[itemSet.peerId] != nil {
|
||||||
|
} else {*/
|
||||||
|
continue
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
validIds.append(itemSet.peerId)
|
validIds.append(itemSet.peerId)
|
||||||
|
|
||||||
let visibleItem: VisibleItem
|
let visibleItem: VisibleItem
|
||||||
@ -159,14 +285,53 @@ public final class StoryPeerListComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hasUnseen = false
|
var hasUnseen = false
|
||||||
if peer.id != component.context.account.peerId {
|
let hasItems = !itemSet.items.isEmpty
|
||||||
for item in itemSet.items {
|
var itemProgress: CGFloat?
|
||||||
if !item.isSeen {
|
if peer.id == component.context.account.peerId {
|
||||||
hasUnseen = true
|
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(
|
let _ = visibleItem.view.update(
|
||||||
transition: itemTransition,
|
transition: itemTransition,
|
||||||
component: AnyComponent(StoryPeerListItemComponent(
|
component: AnyComponent(StoryPeerListItemComponent(
|
||||||
@ -175,19 +340,26 @@ public final class StoryPeerListComponent: Component {
|
|||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
peer: peer,
|
peer: peer,
|
||||||
hasUnseen: hasUnseen,
|
hasUnseen: hasUnseen,
|
||||||
|
hasItems: hasItems,
|
||||||
|
progress: itemProgress,
|
||||||
|
collapseFraction: component.collapseFraction,
|
||||||
|
collapsedWidth: collapsedItemWidth,
|
||||||
|
leftNeighborDistance: leftNeighborDistance,
|
||||||
|
rightNeighborDistance: rightNeighborDistance,
|
||||||
action: component.peerAction
|
action: component.peerAction
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: itemLayout.itemSize
|
containerSize: itemLayout.itemSize
|
||||||
)
|
)
|
||||||
|
|
||||||
let itemFrame = itemLayout.frame(at: i)
|
|
||||||
|
|
||||||
if let itemView = visibleItem.view.view {
|
if let itemView = visibleItem.view.view {
|
||||||
if itemView.superview == nil {
|
if itemView.superview == nil {
|
||||||
self.scrollView.addSubview(itemView)
|
self.scrollView.addSubview(itemView)
|
||||||
}
|
}
|
||||||
|
itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01
|
||||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
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) {
|
if !validIds.contains(id) {
|
||||||
removedIds.append(id)
|
removedIds.append(id)
|
||||||
if let itemView = visibleItem.view.view {
|
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 {
|
for id in removedIds {
|
||||||
self.visibleItems.removeValue(forKey: id)
|
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? {
|
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 {
|
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.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
|
self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne
|
||||||
|
|
||||||
self.sortedItemSets.removeAll(keepingCapacity: true)
|
self.sortedItemSets.removeAll(keepingCapacity: true)
|
||||||
if let state = component.state {
|
if let state = component.state {
|
||||||
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
|
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
|
||||||
self.sortedItemSets.append(state.itemSets[myIndex])
|
self.sortedItemSets.append(state.itemSets[myIndex])
|
||||||
}
|
}
|
||||||
for itemSet in state.itemSets {
|
for i in 0 ..< 4 {
|
||||||
if itemSet.peerId == component.context.account.peerId {
|
for itemSet in state.itemSets {
|
||||||
continue
|
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.ignoreScrolling = false
|
||||||
self.updateScrolling(transition: transition)
|
self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false)
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,137 @@ import SwiftSignalKit
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AvatarNode
|
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 final class StoryPeerListItemComponent: Component {
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let theme: PresentationTheme
|
public let theme: PresentationTheme
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
public let peer: EnginePeer
|
public let peer: EnginePeer
|
||||||
public let hasUnseen: Bool
|
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 let action: (EnginePeer) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -24,6 +149,12 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
peer: EnginePeer,
|
peer: EnginePeer,
|
||||||
hasUnseen: Bool,
|
hasUnseen: Bool,
|
||||||
|
hasItems: Bool,
|
||||||
|
progress: CGFloat?,
|
||||||
|
collapseFraction: CGFloat,
|
||||||
|
collapsedWidth: CGFloat,
|
||||||
|
leftNeighborDistance: CGFloat?,
|
||||||
|
rightNeighborDistance: CGFloat?,
|
||||||
action: @escaping (EnginePeer) -> Void
|
action: @escaping (EnginePeer) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -31,6 +162,12 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.hasUnseen = hasUnseen
|
self.hasUnseen = hasUnseen
|
||||||
|
self.hasItems = hasItems
|
||||||
|
self.progress = progress
|
||||||
|
self.collapseFraction = collapseFraction
|
||||||
|
self.collapsedWidth = collapsedWidth
|
||||||
|
self.leftNeighborDistance = leftNeighborDistance
|
||||||
|
self.rightNeighborDistance = rightNeighborDistance
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,24 +187,70 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
if lhs.hasUnseen != rhs.hasUnseen {
|
if lhs.hasUnseen != rhs.hasUnseen {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class View: HighlightTrackingButton {
|
public final class View: HighlightTrackingButton {
|
||||||
|
private let avatarContainer: UIView
|
||||||
private var avatarNode: AvatarNode?
|
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 let title = ComponentView<Empty>()
|
||||||
|
|
||||||
private var component: StoryPeerListItemComponent?
|
private var component: StoryPeerListItemComponent?
|
||||||
private weak var componentState: EmptyComponentState?
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
self.indicatorCircleView = UIImageView()
|
self.avatarContainer = UIView()
|
||||||
self.indicatorCircleView.isUserInteractionEnabled = false
|
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)
|
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
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
guard let self else {
|
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 {
|
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.component = component
|
||||||
self.componentState = state
|
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
|
let avatarNode: AvatarNode
|
||||||
if let current = self.avatarNode {
|
if let current = self.avatarNode {
|
||||||
avatarNode = current
|
avatarNode = current
|
||||||
} else {
|
} else {
|
||||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||||
self.avatarNode = avatarNode
|
self.avatarNode = avatarNode
|
||||||
|
avatarNode.layer.mask = self.avatarShapeLayer
|
||||||
avatarNode.isUserInteractionEnabled = false
|
avatarNode.isUserInteractionEnabled = false
|
||||||
self.addSubview(avatarNode.view)
|
self.avatarContainer.addSubview(avatarNode.view)
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarSize = CGSize(width: 52.0, height: 52.0)
|
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 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(
|
avatarNode.setPeer(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
peer: component.peer
|
peer: component.peer
|
||||||
)
|
)
|
||||||
avatarNode.updateSize(size: avatarSize)
|
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 {
|
let scaledAvatarSize = effectiveScale * (avatarSize.width + 4.0 - indicatorLineWidth * 2.0)
|
||||||
self.indicatorCircleView.image = nil
|
|
||||||
} else if self.indicatorCircleView.image == nil || hadUnseen != component.hasUnseen {
|
transition.setScale(view: self.avatarContainer, scale: scaledAvatarSize / avatarSize.width)
|
||||||
self.indicatorCircleView.image = generateImage(indicatorFrame.size, rotatedContext: { size, context in
|
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
if component.peer.id == component.context.account.peerId && !component.hasItems {
|
||||||
|
self.indicatorColorLayer.isHidden = true
|
||||||
let lineWidth: CGFloat = 2.0
|
|
||||||
context.setLineWidth(lineWidth)
|
let avatarAddBadgeView: UIImageView
|
||||||
context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
|
var avatarAddBadgeTransition = transition
|
||||||
context.replacePathWithStrokedPath()
|
if let current = self.avatarAddBadgeView {
|
||||||
context.clip()
|
avatarAddBadgeView = current
|
||||||
|
} else {
|
||||||
var locations: [CGFloat] = [1.0, 0.0]
|
avatarAddBadgeTransition = .immediate
|
||||||
let colors: [CGColor]
|
avatarAddBadgeView = UIImageView()
|
||||||
|
self.avatarAddBadgeView = avatarAddBadgeView
|
||||||
if component.hasUnseen {
|
self.avatarContainer.addSubview(avatarAddBadgeView)
|
||||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
}
|
||||||
} else {
|
let badgeSize = CGSize(width: 16.0, height: 16.0)
|
||||||
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
if avatarAddBadgeView.image == nil || themeUpdated {
|
||||||
}
|
avatarAddBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
context.setFillColor(component.theme.list.itemCheckColors.fillColor.cgColor)
|
||||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
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
|
//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(
|
let titleSize = self.title.update(
|
||||||
transition: .immediate,
|
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: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0)
|
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 let titleView = self.title.view {
|
||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
titleView.layer.anchorPoint = CGPoint()
|
titleView.layer.anchorPoint = CGPoint()
|
||||||
@ -171,7 +443,40 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.addSubview(titleView)
|
self.addSubview(titleView)
|
||||||
}
|
}
|
||||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
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
|
return availableSize
|
||||||
|
@ -22,6 +22,7 @@ import LegacyComponents
|
|||||||
import LegacyMediaPickerUI
|
import LegacyMediaPickerUI
|
||||||
import LegacyCamera
|
import LegacyCamera
|
||||||
import AvatarNode
|
import AvatarNode
|
||||||
|
import LocalMediaResources
|
||||||
|
|
||||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||||
private var presentationData: PresentationData
|
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
|
private let context: AccountContext
|
||||||
|
|
||||||
public var rootTabController: TabBarController?
|
public var rootTabController: TabBarController?
|
||||||
@ -270,7 +271,7 @@ public final class TelegramRootController: NavigationController {
|
|||||||
item = TGMediaAsset(phAsset: asset)
|
item = TGMediaAsset(phAsset: asset)
|
||||||
}
|
}
|
||||||
let context = self.context
|
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?()
|
dismissCameraImpl?()
|
||||||
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -362,20 +363,49 @@ public final class TelegramRootController: NavigationController {
|
|||||||
let options = PHImageRequestOptions()
|
let options = PHImageRequestOptions()
|
||||||
options.deliveryMode = .highQualityFormat
|
options.deliveryMode = .highQualityFormat
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
|
switch asset.mediaType {
|
||||||
if let data, let image = UIImage(data: data) {
|
case .image:
|
||||||
Queue.mainQueue().async {
|
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
|
||||||
let _ = (context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy)
|
if let data, let image = UIImage(data: data) {
|
||||||
|> deliverOnMainQueue).start(completed: {
|
Queue.mainQueue().async {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
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()
|
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
|
}, present: { c, a in
|
||||||
|
Loading…
x
Reference in New Issue
Block a user