[WIP] Stories

This commit is contained in:
Ali 2023-05-12 19:29:22 +04:00
parent e8dab90584
commit 8e28d85626
18 changed files with 1216 additions and 184 deletions

View File

@ -742,6 +742,10 @@ public protocol RecentSessionsController: AnyObject {
public protocol AttachmentFileController: AnyObject {
}
public protocol TelegramRootControllerInterface: NavigationController {
func openStoryCamera()
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }

View File

@ -86,8 +86,15 @@ public struct FetchManagerPriorityKey: Comparable {
}
}
public enum FetchManagerLocation: Hashable {
public enum FetchManagerLocation: Hashable, CustomStringConvertible {
case chat(PeerId)
public var description: String {
switch self {
case let .chat(peerId):
return "chat:\(peerId)"
}
}
}
public enum FetchManagerForegroundDirection {

View File

@ -47,12 +47,46 @@ import InviteLinksUI
import ChatFolderLinkPreviewScreen
import StoryContainerScreen
import StoryContentComponent
import StoryPeerListComponent
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
return false
}
let storiesFraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
var visibleStoriesProgress = max(0.0, min(1.0, searchNode.expansionProgress))
visibleStoriesProgress = (1.0 / storiesFraction) * visibleStoriesProgress
visibleStoriesProgress = max(0.0, min(1.0, visibleStoriesProgress))
let searchFieldHeight: CGFloat = 36.0
let searchFraction = searchFieldHeight / searchNode.nominalHeight
let visibleSearchProgress = max(0.0, min(1.0, searchNode.expansionProgress) - 1.0 + searchFraction) / searchFraction
if visibleSearchProgress > 0.0 && visibleSearchProgress < 1.0 {
let offset: CGFloat
if visibleSearchProgress < 0.6 {
offset = navigationBarSearchContentHeight
} else {
offset = 0.0
}
let _ = listNode.scrollToOffsetFromTop(offset, animated: true)
return true
} else if visibleStoriesProgress > 0.0 && visibleStoriesProgress < 1.0 {
let offset: CGFloat
if visibleStoriesProgress < 0.3 {
offset = navigationBarSearchContentHeight + 94.0
} else {
offset = navigationBarSearchContentHeight
}
let _ = listNode.scrollToOffsetFromTop(offset, animated: true)
return true
}
if "".isEmpty {
return false
}
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let offset: CGFloat
if searchNode.expansionProgress < 0.6 {
@ -185,7 +219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var searchContentNode: NavigationBarSearchContentNode?
private let navigationSecondaryContentNode: ASDisplayNode
private var storyPeerListView: ComponentView<Empty>?
private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
@ -210,7 +243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var powerSavingMonitoringDisposable: Disposable?
private var storyListContext: StoryListContext?
public private(set) var storyListContext: StoryListContext?
private var storyListState: StoryListContext.State?
private var storyListStateDisposable: Disposable?
@ -444,7 +477,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
tabsIsEmpty = true
}
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
if case .chatList(.root) = self.location {
self.searchContentNode?.additionalHeight = 94.0
}
self.navigationBar?.secondaryContentHeight = !tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0
enum State: Equatable {
case empty(hasDownloads: Bool)
@ -2123,12 +2159,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
tabsIsEmpty = true
}
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
if case .chatList(.root) = self.location {
self.searchContentNode?.additionalHeight = 94.0
}
if wasEmpty != isEmpty || self.storyPeerListView == nil {
self.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
} else if let componentView = self.storyPeerListView?.view, !componentView.bounds.isEmpty {
self.updateStoryPeerListView(size: componentView.bounds.size, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
if wasEmpty != isEmpty {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
self.requestLayout(transition: transition)
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
} else {
self.updateHeaderStories(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).containedViewLayoutTransition)
}
})
}
@ -2358,33 +2400,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
super.updateNavigationBarLayout(layout, transition: transition)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
self.validLayout = layout
self.updateLayout(layout: layout, transition: transition)
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
self.chatListDisplayNode.scrollToTop()
}
}
private func updateStoryPeerListView(size: CGSize, transition: Transition) {
guard let storyPeerListView = self.storyPeerListView else {
return
}
let _ = storyPeerListView.update(
transition: transition,
component: AnyComponent(StoryPeerListComponent(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
state: self.storyListState,
peerAction: { [weak self] peer in
private func updateHeaderStories(transition: ContainedViewLayoutTransition) {
if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
componentView.storyPeerAction = { [weak self] peer in
guard let self, let storyListContext = self.storyListContext else {
return
}
@ -2401,8 +2420,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peer.id) {
if let peer, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
@ -2411,9 +2430,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
var initialFocusedId: AnyHashable?
if let peer {
initialFocusedId = AnyHashable(peer.id)
}
if !initialContent.contains(where: { slice in
return !slice.items.isEmpty
}) {
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openStoryCamera()
}
return
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
initialFocusedId: AnyHashable(peer.id),
initialFocusedId: initialFocusedId,
initialContent: initialContent,
transitionIn: transitionIn,
transitionOut: { [weak self] peerId in
@ -2421,8 +2455,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peerId) {
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
@ -2437,10 +2471,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(storyContainerScreen)
})
}
)),
environment: {},
containerSize: size
)
let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
var visibleProgress = max(0.0, min(1.0, searchContentNode.expansionProgress))
visibleProgress = (1.0 / fraction) * visibleProgress
visibleProgress = max(0.0, min(1.0, visibleProgress))
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storyListState: self.storyListState, transition: Transition(transition))
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
self.validLayout = layout
self.updateLayout(layout: layout, transition: transition)
self.updateHeaderStories(transition: transition)
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
self.chatListDisplayNode.scrollToTop()
}
}
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -2457,35 +2514,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: layout.size.width, height: 46.0)))
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
var storyPeerListTransition = Transition(transition)
let storyPeerListView: ComponentView<Empty>
if let current = self.storyPeerListView {
storyPeerListView = current
} else {
storyPeerListTransition = .immediate
storyPeerListView = ComponentView()
self.storyPeerListView = storyPeerListView
}
let storyListFrame = CGRect(origin: CGPoint(x: 0.0, y: 46.0 - 0.0), size: CGSize(width: layout.size.width, height: self.storyListHeight + 0.0))
self.updateStoryPeerListView(size: storyListFrame.size, transition: storyPeerListTransition)
if let componentView = storyPeerListView.view {
if componentView.superview == nil {
componentView.alpha = 0.0
self.navigationSecondaryContentNode.view.addSubview(componentView)
}
storyPeerListTransition.setFrame(view: componentView, frame: storyListFrame)
transition.updateAlpha(layer: componentView.layer, alpha: 1.0)
}
} else if let storyPeerListView = self.storyPeerListView {
self.storyPeerListView = nil
if let componentView = storyPeerListView.view {
transition.updateAlpha(layer: componentView.layer, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
if !skipTabContainerUpdate {
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
}
@ -2930,7 +2958,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.didSetupTabs = true
if strongSelf.displayNavigationBar {
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + strongSelf.storyListHeight
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
if case .chatList(.root) = strongSelf.location {
strongSelf.searchContentNode?.additionalHeight = 94.0
}
strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false)
}
@ -3309,6 +3340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
}
strongSelf.searchContentNode?.additionalHeight = 0.0
activate(filter != .downloads)
@ -3362,7 +3394,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
filterContainerNode = searchContentNode.filterContainerNode
if let filterContainerNode = filterContainerNode, let snapshotView = filterContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = filterContainerNode.frame
snapshotView.frame = filterContainerNode.frame//.offsetBy(dx: self.navigationSecondaryContentNode.frame.minX, dy: self.navigationSecondaryContentNode.frame.minY)
filterContainerNode.view.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
@ -3379,10 +3411,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if let searchContentNode = self.searchContentNode {
let previousFrame = searchContentNode.placeholderNode.frame
if case .chatList(.root) = self.location {
searchContentNode.placeholderNode.frame = previousFrame.offsetBy(dx: 0.0, dy: 94.0)
}
completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
searchContentNode.placeholderNode.frame = previousFrame
}
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
if case .chatList(.root) = self.location {
self.searchContentNode?.additionalHeight = 94.0
}
self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false)
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate

View File

@ -373,6 +373,10 @@ private final class ChatListContainerItemNode: ASDisplayNode {
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode)
if let controller, case .chatList(groupId: .root) = controller.location {
self.listNode.scrollHeightTopInset = navigationBarSearchContentHeight + 94.0
}
super.init()
self.addSubnode(self.listNode)
@ -1481,7 +1485,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
private(set) var inlineStackContainerNode: ChatListContainerNode?
private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
private(set) var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
private var tapRecognizer: UITapGestureRecognizer?
var navigationBar: NavigationBar?

View File

@ -1184,6 +1184,12 @@ public final class ChatListNode: ListView {
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
public var scrollHeightTopInset: CGFloat {
didSet {
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
}
}
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
self.context = context
self.location = location
@ -1204,12 +1210,14 @@ public final class ChatListNode: ListView {
self.theme = theme
self.scrollHeightTopInset = navigationBarSearchContentHeight
super.init()
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
self.verticalScrollIndicatorFollowsOverscroll = true
self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
@ -3140,7 +3148,7 @@ public final class ChatListNode: ListView {
case let .known(value) where abs(value) < .ulpOfOne:
offset = 0.0
default:
offset = -navigationBarSearchContentHeight
offset = -self.scrollHeightTopInset
}
}
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
@ -3153,7 +3161,7 @@ public final class ChatListNode: ListView {
var isNavigationHidden: Bool {
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
return false
case .none:
return false
@ -3165,11 +3173,11 @@ public final class ChatListNode: ListView {
var isNavigationInAFinalState: Bool {
switch self.visibleContentOffset() {
case let .known(value):
if value < navigationBarSearchContentHeight - 1.0 {
if value < self.scrollHeightTopInset - 1.0 {
if abs(value - 0.0) < 1.0 {
return true
}
if abs(value - navigationBarSearchContentHeight) < 1.0 {
if abs(value - self.scrollHeightTopInset) < 1.0 {
return true
}
return false
@ -3187,9 +3195,9 @@ public final class ChatListNode: ListView {
}
var scrollToItem: ListViewScrollToItem?
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
if isNavigationHidden {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-self.scrollHeightTopInset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
default:
if !isNavigationHidden {

View File

@ -1395,7 +1395,6 @@ open class NavigationBar: ASDisplayNode {
if let customHeaderContentView = self.customHeaderContentView {
let headerSize = CGSize(width: size.width, height: nominalHeight)
//customHeaderContentView.update(size: headerSize, transition: transition)
transition.updateFrame(view: customHeaderContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOrigin), size: headerSize))
}
@ -1732,12 +1731,15 @@ open class NavigationBar: ASDisplayNode {
if let result = self.additionalContentNode.view.hitTest(self.view.convert(point, to: self.additionalContentNode.view), with: event) {
return result
}
if self.frame.minY > -10.0, let customHeaderContentView = self.customHeaderContentView, let result = customHeaderContentView.hitTest(self.view.convert(point, to: customHeaderContentView), with: event) {
return result
}
guard let result = super.hitTest(point, with: event) else {
return nil
}
if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) {
return nil
}

View File

@ -161,6 +161,9 @@ private final class FetchManagerCategoryContext {
previousPriorityKey = nil
let (mediaReference, resourceReference, statsCategory, episode) = takeNew()
entry = FetchManagerLocationEntry(id: id, episode: episode, mediaReference: mediaReference, resourceReference: resourceReference, statsCategory: statsCategory)
Logger.shared.log("FetchManager", "[\(entry.id.location)] Adding entry \(entry.resourceReference.resource.id.stringRepresentation) (\(self.entries.count) in queue)")
self.entries[id] = entry
} else {
return
@ -236,7 +239,7 @@ private final class FetchManagerCategoryContext {
}
activeContext.disposable?.dispose()
let postbox = self.postbox
Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
var userLocation: MediaResourceUserLocation = .other
switch entry.id.location {
@ -401,7 +404,7 @@ private final class FetchManagerCategoryContext {
} else if ranges.isEmpty {
} else {
let postbox = self.postbox
Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
var userLocation: MediaResourceUserLocation = .other
switch entry.id.location {

View File

@ -19,6 +19,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
private var disabledOverlay: ASDisplayNode?
public private(set) var expansionProgress: CGFloat = 1.0
public var additionalHeight: CGFloat = 0.0
private var validLayout: (CGSize, CGFloat, CGFloat)?
@ -125,7 +127,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, textColor, fillColor, backgroundColor, transition)
searchBarApply()
let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: 8.0 + overscrollProgress * fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight))
let _ = overscrollProgress
let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: size.height + (1.0 - visibleProgress) * fieldHeight - 8.0 - fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight))
transition.updateFrame(node: self.placeholderNode, frame: searchBarFrame)
self.placeholderHeight = searchBarHeight
@ -151,7 +154,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
}
override public var nominalHeight: CGFloat {
return navigationBarSearchContentHeight
return navigationBarSearchContentHeight + self.additionalHeight
}
override public var mode: NavigationBarContentMode {

View File

@ -5,6 +5,7 @@ import TelegramApi
public enum EngineStoryInputMedia {
case image(dimensions: PixelDimensions, data: Data)
case video(dimensions: PixelDimensions, duration: Int, resource: TelegramMediaResource)
}
public struct EngineStoryPrivacy: Equatable {
@ -141,6 +142,124 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, priva
}
|> switchToLatest
}
case let .video(dimensions, duration, resource):
let fileMedia = TelegramMediaFile(
fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: MediaId.Id.random(in: MediaId.Id.min ... MediaId.Id.max)),
partialReference: nil,
resource: resource,
previewRepresentations: [],
videoThumbnails: [],
immediateThumbnailData: nil,
mimeType: "video/mp4",
size: nil,
attributes: [
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming)
]
)
let contentToUpload = messageContentToUpload(
network: account.network,
postbox: account.postbox,
auxiliaryMethods: account.auxiliaryMethods,
transformOutgoingMessageMedia: nil,
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
revalidationContext: account.mediaReferenceRevalidationContext,
forceReupload: true,
isGrouped: false,
peerId: account.peerId,
messageId: nil,
attributes: [],
text: "",
media: [fileMedia]
)
let contentSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>
switch contentToUpload {
case let .immediate(result, _):
contentSignal = .single(result)
case let .signal(signal, _):
contentSignal = signal
}
return contentSignal
|> map(Optional.init)
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
var privacyRules: [Api.InputPrivacyRule]
switch privacy.base {
case .everyone:
privacyRules = [.inputPrivacyValueAllowAll]
case .contacts:
privacyRules = [.inputPrivacyValueAllowContacts]
case .closeFriends:
privacyRules = [.inputPrivacyValueAllowCloseFriends]
}
var privacyUsers: [Api.InputUser] = []
var privacyChats: [Int64] = []
for peerId in privacy.additionallyIncludePeers {
if let peer = transaction.getPeer(peerId) {
if let _ = peer as? TelegramUser {
if let inputUser = apiInputUser(peer) {
privacyUsers.append(inputUser)
}
} else if peer is TelegramGroup || peer is TelegramChannel {
privacyChats.append(peer.id.id._internalGetInt64Value())
}
}
}
if !privacyUsers.isEmpty {
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
}
if !privacyChats.isEmpty {
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
}
switch result {
case let .content(content):
switch content.content {
case let .media(inputMedia, _):
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: privacyRules))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
for update in updates.allUpdates {
if case let .updateStories(stories) = update {
switch stories {
case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _):
if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 {
switch apiStories[0] {
case let .storyItem(_, _, _, _, _, media, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: fileMedia, to: parsedMedia, postbox: account.postbox, force: true)
}
default:
break
}
}
}
}
}
account.stateManager.addUpdates(updates)
}
return .complete()
}
default:
return .complete()
}
default:
return .complete()
}
}
|> switchToLatest
}
}
}

View File

@ -107,14 +107,23 @@ public final class StoryListContext {
public struct State: Equatable {
public var itemSets: [PeerItemSet]
public var uploadProgress: CGFloat?
public var loadMoreToken: LoadMoreToken?
public init(itemSets: [PeerItemSet], loadMoreToken: LoadMoreToken?) {
public init(itemSets: [PeerItemSet], uploadProgress: CGFloat?, loadMoreToken: LoadMoreToken?) {
self.itemSets = itemSets
self.uploadProgress = uploadProgress
self.loadMoreToken = loadMoreToken
}
}
private final class UploadContext {
let disposable = MetaDisposable()
init() {
}
}
private final class Impl {
private let queue: Queue
private let account: Account
@ -127,6 +136,12 @@ public final class StoryListContext {
private var updatesDisposable: Disposable?
private var peerDisposables: [PeerId: Disposable] = [:]
private var uploadContexts: [UploadContext] = [] {
didSet {
self.stateValue.uploadProgress = self.uploadContexts.isEmpty ? nil : 0.0
}
}
private var stateValue: State {
didSet {
self.state.set(.single(self.stateValue))
@ -139,9 +154,21 @@ public final class StoryListContext {
self.account = account
self.scope = scope
self.stateValue = State(itemSets: [], loadMoreToken: LoadMoreToken(value: nil))
self.stateValue = State(itemSets: [], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
self.state.set(.single(self.stateValue))
let _ = (account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(account.peerId)
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.stateValue = State(itemSets: [
PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0)
], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
})
self.updatesDisposable = (account.stateManager.storyUpdates
|> deliverOn(queue)).start(next: { [weak self] updates in
if updates.isEmpty {
@ -150,6 +177,11 @@ public final class StoryListContext {
let _ = account.postbox.transaction({ transaction -> [PeerId: Peer] in
var peers: [PeerId: Peer] = [:]
if let peer = transaction.getPeer(account.peerId) {
peers[peer.id] = peer
}
for update in updates {
switch update {
case let .added(peerId, _):
@ -198,7 +230,21 @@ public final class StoryListContext {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items.remove(at: index)
}
items.append(item)
if peerId == self.account.peerId {
items.append(Item(
id: item.id,
timestamp: item.timestamp,
media: item.media,
isSeen: false,
seenCount: item.seenCount,
seenPeers: item.seenPeers,
privacy: item.privacy
))
} else {
items.append(item)
}
items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp {
return lhsItem.timestamp < rhsItem.timestamp
@ -263,6 +309,12 @@ public final class StoryListContext {
return lhsItem.id > rhsItem.id
})
if !itemSets.contains(where: { $0.peerId == self.account.peerId }) {
if let peer = peers[self.account.peerId] {
itemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0)
}
}
self.stateValue.itemSets = itemSets
})
})
@ -398,6 +450,21 @@ public final class StoryListContext {
}
}
func upload(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) {
let uploadContext = UploadContext()
self.uploadContexts.append(uploadContext)
uploadContext.disposable.set((_internal_uploadStory(account: self.account, media: media, privacy: privacy)
|> deliverOn(self.queue)).start(next: { _ in
}, completed: { [weak self, weak uploadContext] in
guard let `self` = self, let uploadContext = uploadContext else {
return
}
if let index = self.uploadContexts.firstIndex(where: { $0 === uploadContext }) {
self.uploadContexts.remove(at: index)
}
}))
}
func loadMore(refresh: Bool) {
if self.isLoadingMore {
return
@ -528,6 +595,12 @@ public final class StoryListContext {
}
}
if !parsedItemSets.contains(where: { $0.peerId == account.peerId }) {
if let peer = transaction.getPeer(account.peerId) {
parsedItemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0)
}
}
return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) })
}
}
@ -589,7 +662,7 @@ public final class StoryListContext {
return lhsItem.id > rhsItem.id
})
self.stateValue = State(itemSets: itemSets, loadMoreToken: result.1)
self.stateValue = State(itemSets: itemSets, uploadProgress: self.stateValue.uploadProgress, loadMoreToken: result.1)
}))
}
@ -639,4 +712,10 @@ public final class StoryListContext {
impl.delete(id: id)
}
}
public func upload(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) {
self.impl.with { impl in
impl.upload(media: media, privacy: privacy)
}
}
}

View File

@ -16,9 +16,11 @@ swift_library(
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/ChatListTitleView",
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/AppBundle",
"//submodules/AsyncDisplayKit",
"//submodules/AnimationUI",
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
],
visibility = [
"//visibility:public",

View File

@ -6,6 +6,8 @@ import TelegramPresentationData
import AccountContext
import ChatListTitleView
import AppBundle
import StoryPeerListComponent
import TelegramCore
public final class HeaderNetworkStatusComponent: Component {
public enum Content: Equatable {
@ -272,10 +274,13 @@ public final class ChatListHeaderComponent: Component {
var backButtonView: BackButtonView?
let titleOffsetContainer: UIView
let titleScaleContainer: UIView
let titleTextView: ImmediateTextView
var titleContentView: ComponentView<Empty>?
var chatListTitleView: ChatListTitleView?
var contentOffsetFraction: CGFloat = 0.0
init(
backPressed: @escaping () -> Void,
openStatusSetup: @escaping (UIView) -> Void,
@ -288,16 +293,18 @@ public final class ChatListHeaderComponent: Component {
self.leftButtonOffsetContainer = UIView()
self.rightButtonOffsetContainer = UIView()
self.titleOffsetContainer = UIView()
self.titleScaleContainer = UIView()
self.titleTextView = ImmediateTextView()
super.init(frame: CGRect())
self.addSubview(self.titleOffsetContainer)
self.titleOffsetContainer.addSubview(self.titleScaleContainer)
self.addSubview(self.leftButtonOffsetContainer)
self.addSubview(self.rightButtonOffsetContainer)
self.titleOffsetContainer.addSubview(self.titleTextView)
self.titleScaleContainer.addSubview(self.titleTextView)
}
required init?(coder: NSCoder) {
@ -329,10 +336,28 @@ public final class ChatListHeaderComponent: Component {
return nil
}
func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: Transition) {
if self.contentOffsetFraction == contentOffsetFraction {
return
}
self.contentOffsetFraction = contentOffsetFraction
let scale = 1.0 * (1.0 - contentOffsetFraction) + 0.001 * contentOffsetFraction
let translation = -44.0 * contentOffsetFraction * 0.5
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, 0.0, translation, 0.0)
transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform)
transition.setScale(view: self.titleScaleContainer, scale: scale)
transition.setAlpha(view: self.titleScaleContainer, alpha: 1.0 - contentOffsetFraction)
}
func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in
completion()
})
transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
if let backButtonView = self.backButtonView {
@ -356,6 +381,7 @@ public final class ChatListHeaderComponent: Component {
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in
completion()
})
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0))
transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size))
if let backButtonView = self.backButtonView {
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
@ -373,7 +399,45 @@ public final class ChatListHeaderComponent: Component {
}
}
func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in
})
transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in
completion()
})
if let backButtonView = self.backButtonView {
transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in
})
}
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size))
transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
}
func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in
completion()
})
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0))
transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size))
if let backButtonView = self.backButtonView {
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0))
transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size))
transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0))
}
}
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, size: CGSize, transition: Transition) {
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size))
self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
let buttonSpacing: CGFloat = 8.0
@ -530,7 +594,7 @@ public final class ChatListHeaderComponent: Component {
)
if let titleContentComponentView = titleContentView.view {
if titleContentComponentView.superview == nil {
self.titleOffsetContainer.addSubview(titleContentComponentView)
self.titleScaleContainer.addSubview(titleContentComponentView)
}
titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize))
}
@ -551,7 +615,7 @@ public final class ChatListHeaderComponent: Component {
chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer)
chatListTitleView.manualLayout = true
self.chatListTitleView = chatListTitleView
self.titleOffsetContainer.addSubview(chatListTitleView)
self.titleScaleContainer.addSubview(chatListTitleView)
}
let chatListTitleContentSize = size
@ -591,6 +655,10 @@ public final class ChatListHeaderComponent: Component {
private var primaryContentView: ContentView?
private var secondaryContentView: ContentView?
private var storyOffsetFraction: CGFloat = 0.0
private var storyPeerList: ComponentView<Empty>?
public var storyPeerAction: ((EnginePeer?) -> Void)?
private var effectiveContentView: ContentView? {
return self.secondaryContentView ?? self.primaryContentView
@ -598,6 +666,8 @@ public final class ChatListHeaderComponent: Component {
override init(frame: CGRect) {
super.init(frame: frame)
self.storyOffsetFraction = 1.0
}
required public init?(coder: NSCoder) {
@ -643,6 +713,107 @@ public final class ChatListHeaderComponent: Component {
}
}
public func storyPeerListView() -> StoryPeerListComponent.View? {
return self.storyPeerList?.view as? StoryPeerListComponent.View
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if !subview.isUserInteractionEnabled || subview.alpha < 0.01 || subview.isHidden {
continue
}
if subview === self.storyPeerList?.view {
continue
}
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if let storyPeerListView = self.storyPeerList?.view {
if let result = storyPeerListView.hitTest(self.convert(point, to: storyPeerListView), with: event) {
return result
}
}
let defaultResult = super.hitTest(point, with: event)
if let defaultResult, defaultResult !== self {
return defaultResult
}
return defaultResult
}
public func updateStories(offset: CGFloat, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, storyListState: StoryListContext.State?, transition: Transition) {
var storyOffsetFraction: CGFloat = 1.0
if let storyListState, storyListState.itemSets.count > 1 {
storyOffsetFraction = offset
}
self.storyOffsetFraction = storyOffsetFraction
let storyPeerList: ComponentView<Empty>
var storyListTransition = transition
if let current = self.storyPeerList {
storyPeerList = current
} else {
storyListTransition = .immediate
storyPeerList = ComponentView()
self.storyPeerList = storyPeerList
}
if !self.bounds.isEmpty {
let _ = storyPeerList.update(
transition: storyListTransition,
component: AnyComponent(StoryPeerListComponent(
context: context,
theme: theme,
strings: strings,
state: storyListState,
collapseFraction: 1.0 - offset,
peerAction: { [weak self] peer in
guard let self else {
return
}
self.storyPeerAction?(peer)
}
)),
environment: {},
containerSize: CGSize(width: self.bounds.width, height: 94.0)
)
if let storyPeerListComponentView = storyPeerList.view {
if storyPeerListComponentView.superview == nil {
self.addSubview(storyPeerListComponentView)
}
let storyPeerListMinOffset: CGFloat = -7.0
let storyPeerListMaxOffset: CGFloat = self.bounds.height + 8.0
let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - offset) + storyPeerListMaxOffset * offset
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: storyPeerListPosition), size: CGSize(width: self.bounds.width, height: 94.0)))
var storyListAlpha: CGFloat = 1.0
if let storyListState, storyListState.itemSets.count > 1 {
} else {
storyListAlpha = offset
}
storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha)
}
if let primaryContentView = self.primaryContentView {
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition)
}
if let secondaryContentView = self.secondaryContentView {
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition)
}
}
}
private func updateContentStoryOffsets(transition: Transition) {
}
func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.state = state
@ -681,6 +852,8 @@ public final class ChatListHeaderComponent: Component {
}
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, size: availableSize, transition: primaryContentTransition)
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
} else if let primaryContentView = self.primaryContentView {
self.primaryContentView = nil
primaryContentView.removeFromSuperview()
@ -719,23 +892,42 @@ public final class ChatListHeaderComponent: Component {
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, size: availableSize, transition: secondaryContentTransition)
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
if let primaryContentView = self.primaryContentView {
if let previousComponent = previousComponent, previousComponent.secondaryContent == nil {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
if self.storyOffsetFraction < 0.8 {
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
} else {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
}
}
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
if self.storyOffsetFraction < 0.8 {
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
} else {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
}
}
} else if let secondaryContentView = self.secondaryContentView {
self.secondaryContentView = nil
if let primaryContentView = self.primaryContentView {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
secondaryContentView?.removeFromSuperview()
})
if self.storyOffsetFraction < 0.8 {
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
secondaryContentView?.removeFromSuperview()
})
} else {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
secondaryContentView?.removeFromSuperview()
})
}
} else {
secondaryContentView.removeFromSuperview()
}

View File

@ -687,10 +687,6 @@ public final class StoryItemSetContainerComponent: Component {
self.currentSliceDisposable?.dispose()
if let focusedItemId = self.focusedItemId {
if let item = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
item.markAsSeen?()
}
self.currentSliceDisposable = (component.initialItemSlice.update(
component.initialItemSlice,
focusedItemId

View File

@ -24,6 +24,7 @@ public enum StoryChatContent {
position: items.count,
component: AnyComponent(StoryItemContentComponent(
context: context,
peerId: peerId,
item: item
)),
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
@ -57,7 +58,7 @@ public enum StoryChatContent {
var sliceFocusedItemId: AnyHashable?
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) {
sliceFocusedItemId = AnyHashable(focusItem)
} else if itemSet.peerId != context.account.peerId {
} else {
if let id = itemSet.items.first(where: { !$0.isSeen })?.id {
sliceFocusedItemId = AnyHashable(id)
}

View File

@ -16,10 +16,12 @@ final class StoryItemContentComponent: Component {
typealias EnvironmentType = StoryContentItem.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let item: StoryListContext.Item
init(context: AccountContext, item: StoryListContext.Item) {
init(context: AccountContext, peerId: EnginePeer.Id, item: StoryListContext.Item) {
self.context = context
self.peerId = peerId
self.item = item
}
@ -27,6 +29,9 @@ final class StoryItemContentComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.item != rhs.item {
return false
}
@ -99,6 +104,9 @@ final class StoryItemContentComponent: Component {
private var currentProgressTimerValue: Double = 0.0
private var videoProgressDisposable: Disposable?
private var markedAsSeen: Bool = false
private var contentLoaded: Bool = false
private var videoPlaybackStatus: MediaPlayerStatus?
private let hierarchyTrackingLayer: HierarchyTrackingLayer
@ -186,7 +194,7 @@ final class StoryItemContentComponent: Component {
private func updateIsProgressPaused() {
if let videoNode = self.videoNode {
if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy {
if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy {
videoNode.play()
} else {
videoNode.pause()
@ -198,7 +206,7 @@ final class StoryItemContentComponent: Component {
}
private func updateProgressTimer() {
let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy
let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
if needsTimer {
if self.currentProgressTimer == nil {
@ -206,13 +214,20 @@ final class StoryItemContentComponent: Component {
timeout: 1.0 / 60.0,
repeat: true,
completion: { [weak self] in
guard let self, !self.isProgressPaused, self.hierarchyTrackingLayer.isInHierarchy else {
guard let self, !self.isProgressPaused, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else {
return
}
if self.videoNode != nil {
if case .file = self.currentMessageMedia {
self.updateVideoPlaybackProgress()
} else {
if !self.markedAsSeen {
self.markedAsSeen = true
if let component = self.component {
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start()
}
}
#if DEBUG && false
let currentProgressTimerLimit: Double = 5 * 60.0
#else
@ -273,6 +288,15 @@ final class StoryItemContentComponent: Component {
progress = min(1.0, progress)
currentProgress = progress
if isPlaying {
if !self.markedAsSeen {
self.markedAsSeen = true
if let component = self.component {
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start()
}
}
}
}
let clippedProgress = max(0.0, min(1.0, currentProgress))
@ -325,6 +349,8 @@ final class StoryItemContentComponent: Component {
}
}
case let .file(file):
self.contentLoaded = true
signal = chatMessageVideo(
postbox: component.context.account.postbox,
userLocation: .other,
@ -362,7 +388,15 @@ final class StoryItemContentComponent: Component {
self.fetchDisposable?.dispose()
self.fetchDisposable = nil
if let fetchSignal {
self.fetchDisposable = fetchSignal.start()
self.fetchDisposable = (fetchSignal |> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
if !self.contentLoaded {
self.contentLoaded = true
self.state?.updated(transition: .immediate)
}
})
}
}
@ -408,7 +442,8 @@ final class StoryItemContentComponent: Component {
})
}
}
self.updateProgressTimer()
self.updateIsProgressPaused()
return availableSize
}

View File

@ -14,19 +14,22 @@ public final class StoryPeerListComponent: Component {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let state: StoryListContext.State?
public let peerAction: (EnginePeer) -> Void
public let collapseFraction: CGFloat
public let peerAction: (EnginePeer?) -> Void
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
state: StoryListContext.State?,
peerAction: @escaping (EnginePeer) -> Void
collapseFraction: CGFloat,
peerAction: @escaping (EnginePeer?) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.state = state
self.collapseFraction = collapseFraction
self.peerAction = peerAction
}
@ -43,6 +46,9 @@ public final class StoryPeerListComponent: Component {
if lhs.state != rhs.state {
return false
}
if lhs.collapseFraction != rhs.collapseFraction {
return false
}
return true
}
@ -90,6 +96,7 @@ public final class StoryPeerListComponent: Component {
}
public final class View: UIView, UIScrollViewDelegate {
private let collapsedButton: HighlightableButton
private let scrollView: ScrollView
private var ignoreScrolling: Bool = false
@ -98,10 +105,14 @@ public final class StoryPeerListComponent: Component {
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
private let title = ComponentView<Empty>()
private var component: StoryPeerListComponent?
private weak var state: EmptyComponentState?
public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton()
self.scrollView = ScrollView()
self.scrollView.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
@ -116,13 +127,43 @@ public final class StoryPeerListComponent: Component {
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.addSubview(self.collapsedButton)
self.collapsedButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.layer.allowsGroupOpacity = true
self.alpha = 0.6
} else {
self.alpha = 1.0
self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.25, completion: { [weak self] finished in
guard let self, finished else {
return
}
self.layer.allowsGroupOpacity = false
})
}
}
self.collapsedButton.addTarget(self, action: #selector(self.collapsedButtonPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func collapsedButtonPressed() {
guard let component = self.component else {
return
}
component.peerAction(nil)
}
public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? {
if self.collapsedButton.isUserInteractionEnabled {
return nil
}
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
return itemView.transitionView()
}
@ -131,21 +172,106 @@ public final class StoryPeerListComponent: Component {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
self.updateScrolling(transition: .immediate, keepVisibleUntilCompletion: false)
}
}
private func updateScrolling(transition: Transition) {
private func updateScrolling(transition: Transition, keepVisibleUntilCompletion: Bool) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
var hasStories: Bool = false
if let state = component.state, state.itemSets.count > 1 {
hasStories = true
}
let titleSpacing: CGFloat = 8.0
let titleText: String
let storyCount = self.sortedItemSets.count - 1
if storyCount <= 0 {
titleText = "No Stories"
} else {
if storyCount == 1 {
titleText = "1 Story"
} else {
titleText = "\(storyCount) Stories"
}
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)),
environment: {},
containerSize: CGSize(width: itemLayout.containerSize.width, height: 100.0)
)
let collapseStartIndex = 1
let collapsedItemWidth: CGFloat = 24.0
let collapsedItemDistance: CGFloat = 14.0
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItemSets.count - collapseStartIndex, 3))
var collapsedContentWidth: CGFloat = 0.0
if collapsedItemCount > 0 {
collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
collapsedContentWidth += titleSpacing
}
let collapseEndIndex = collapseStartIndex + Int(collapsedItemCount)
let _ = collapseEndIndex
let titleOffset = collapsedContentWidth
collapsedContentWidth += titleSize.width
let collapsedContentOrigin: CGFloat
let collapsedItemOffsetY: CGFloat
let itemScale: CGFloat
if hasStories {
collapsedContentOrigin = floor((itemLayout.containerSize.width - collapsedContentWidth) * 0.5)
itemScale = 1.0
collapsedItemOffsetY = 0.0
} else {
collapsedContentOrigin = itemLayout.frame(at: 0).minX + 30.0
itemScale = 1.0//1.0 * (1.0 - component.collapseFraction) + 0.001 * component.collapseFraction
collapsedItemOffsetY = 16.0
}
let titleFrame = CGRect(origin: CGPoint(x: component.collapseFraction * (collapsedContentOrigin + titleOffset) + (1.0 - component.collapseFraction) * (itemLayout.containerSize.width), y: 19.0/* * component.collapseFraction + (1.0 - component.collapseFraction) * (-40.0)*/), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.scrollView.addSubview(titleView)
}
transition.setPosition(view: titleView, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY))
transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
var titleAlpha: CGFloat = pow(component.collapseFraction, 1.5)
if !hasStories {
titleAlpha = 0.0
}
transition.setAlpha(view: titleView, alpha: titleAlpha)
transition.setScale(view: titleView, scale: (component.collapseFraction) * 1.0 + (1.0 - component.collapseFraction) * 0.001)
}
let visibleBounds = self.scrollView.bounds
var validIds: [EnginePeer.Id] = []
for i in 0 ..< self.sortedItemSets.count {
let itemSet = self.sortedItemSets[i]
guard let peer = itemSet.peer else {
continue
}
let regularItemFrame = itemLayout.frame(at: i)
if !visibleBounds.intersects(regularItemFrame) {
/*if keepVisibleUntilCompletion && self.visibleItems[itemSet.peerId] != nil {
} else {*/
continue
//}
}
validIds.append(itemSet.peerId)
let visibleItem: VisibleItem
@ -159,14 +285,53 @@ public final class StoryPeerListComponent: Component {
}
var hasUnseen = false
if peer.id != component.context.account.peerId {
for item in itemSet.items {
if !item.isSeen {
hasUnseen = true
}
let hasItems = !itemSet.items.isEmpty
var itemProgress: CGFloat?
if peer.id == component.context.account.peerId {
itemProgress = component.state?.uploadProgress
//itemProgress = 0.0
}
for item in itemSet.items {
if !item.isSeen {
hasUnseen = true
}
}
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
let itemFrame = regularItemFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction)
var leftItemFrame: CGRect?
var rightItemFrame: CGRect?
var itemAlpha: CGFloat = 1.0
if i >= collapseStartIndex && i <= (collapseStartIndex + 2) {
if i != collapseStartIndex {
let regularLeftItemFrame = itemLayout.frame(at: i - 1)
let collapsedLeftItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularLeftItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularLeftItemFrame.height))
leftItemFrame = regularLeftItemFrame.interpolate(to: collapsedLeftItemFrame, amount: component.collapseFraction)
}
if i != collapseStartIndex + 2 {
let regularRightItemFrame = itemLayout.frame(at: i - 1)
let collapsedRightItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularRightItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularRightItemFrame.height))
rightItemFrame = regularRightItemFrame.interpolate(to: collapsedRightItemFrame, amount: component.collapseFraction)
}
} else {
itemAlpha = pow(1.0 - component.collapseFraction, 1.5)
}
var leftNeighborDistance: CGFloat?
var rightNeighborDistance: CGFloat?
if let leftItemFrame {
leftNeighborDistance = abs(leftItemFrame.midX - itemFrame.midX)
}
if let rightItemFrame {
rightNeighborDistance = abs(rightItemFrame.midX - itemFrame.midX)
}
let _ = visibleItem.view.update(
transition: itemTransition,
component: AnyComponent(StoryPeerListItemComponent(
@ -175,19 +340,26 @@ public final class StoryPeerListComponent: Component {
strings: component.strings,
peer: peer,
hasUnseen: hasUnseen,
hasItems: hasItems,
progress: itemProgress,
collapseFraction: component.collapseFraction,
collapsedWidth: collapsedItemWidth,
leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance,
action: component.peerAction
)),
environment: {},
containerSize: itemLayout.itemSize
)
let itemFrame = itemLayout.frame(at: i)
if let itemView = visibleItem.view.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
}
itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01
itemTransition.setFrame(view: itemView, frame: itemFrame)
itemTransition.setAlpha(view: itemView, alpha: itemAlpha)
itemTransition.setScale(view: itemView, scale: itemScale)
}
}
@ -196,13 +368,21 @@ public final class StoryPeerListComponent: Component {
if !validIds.contains(id) {
removedIds.append(id)
if let itemView = visibleItem.view.view {
itemView.removeFromSuperview()
if keepVisibleUntilCompletion && !transition.animation.isImmediate {
transition.attachAnimation(view: itemView, id: "keep", completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removedIds {
self.visibleItems.removeValue(forKey: id)
}
transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.containerSize.height - 8.0)))
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
@ -210,19 +390,41 @@ public final class StoryPeerListComponent: Component {
}
func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
if self.component != nil {
if component.collapseFraction != 0.0 && self.scrollView.bounds.minX != 0.0 {
self.ignoreScrolling = true
let scrollingDistance = self.scrollView.bounds.minX
self.scrollView.bounds = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)
let tempTransition = Transition(animation: .curve(duration: 0.3, curve: .spring))
self.updateScrolling(transition: tempTransition, keepVisibleUntilCompletion: true)
tempTransition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: scrollingDistance, y: 0.0), to: CGPoint(), additive: true)
self.ignoreScrolling = false
}
}
self.component = component
self.state = state
self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne
self.sortedItemSets.removeAll(keepingCapacity: true)
if let state = component.state {
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
self.sortedItemSets.append(state.itemSets[myIndex])
}
for itemSet in state.itemSets {
if itemSet.peerId == component.context.account.peerId {
continue
for i in 0 ..< 4 {
for itemSet in state.itemSets {
if itemSet.peerId == component.context.account.peerId {
continue
}
if i == 0 {
self.sortedItemSets.append(itemSet)
} else {
self.sortedItemSets.append(StoryListContext.PeerItemSet(peerId: EnginePeer.Id(namespace: itemSet.peerId.namespace, id: EnginePeer.Id.Id._internalFromInt64Value(itemSet.peerId.id._internalGetInt64Value() + Int64(i))), peer: itemSet.peer, items: itemSet.items, totalCount: itemSet.totalCount))
}
}
self.sortedItemSets.append(itemSet)
}
}
@ -243,7 +445,7 @@ public final class StoryPeerListComponent: Component {
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false)
return availableSize
}

View File

@ -10,12 +10,137 @@ import SwiftSignalKit
import TelegramPresentationData
import AvatarNode
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)
if distance > radius * 2.0 || distance == 0.0 {
return nil
}
let x1 = center.x
let y1 = center.y
let x2 = otherCenter.x
let y2 = otherCenter.y
let r1 = radius
let r2 = radius
let R = distance
let ix1: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1)
let ix2: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1)
let iy1: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2)
let iy2: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2)
var v1 = CGPoint(x: ix1 - center.x, y: iy1 - center.y)
let length1 = sqrt(v1.x * v1.x + v1.y * v1.y)
v1.x /= length1
v1.y /= length1
var v2 = CGPoint(x: ix2 - center.x, y: iy2 - center.y)
let length2 = sqrt(v2.x * v2.x + v2.y * v2.y)
v2.x /= length2
v2.y /= length2
var point1Angle = atan(v1.y / v1.x)
var point2Angle = atan(v2.y / v2.x)
if distanceVector.x < 0.0 {
point1Angle += CGFloat.pi
point2Angle += CGFloat.pi
}
return (point1Angle, point2Angle)
}
private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat) -> CGPath {
let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
let path = CGMutablePath()
if let leftAngles, let rightAngles {
path.addArc(center: center, radius: radius, startAngle: leftAngles.point1Angle, endAngle: rightAngles.point2Angle, clockwise: true)
path.move(to: CGPoint(x: center.x + cos(rightAngles.point1Angle) * radius, y: center.y + sin(rightAngles.point1Angle) * radius))
path.addArc(center: center, radius: radius, startAngle: rightAngles.point1Angle, endAngle: leftAngles.point2Angle, clockwise: true)
} else if let angles = leftAngles ?? rightAngles {
path.addArc(center: center, radius: radius, startAngle: angles.point1Angle, endAngle: angles.point2Angle, clockwise: true)
} else {
path.addEllipse(in: CGRect(origin: CGPoint(x: center.x - radius, y: center.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
return path
}
private final class StoryProgressLayer: SimpleShapeLayer {
private struct Params: Equatable {
var size: CGSize
var lineWidth: CGFloat
}
private var currentParams: Params?
override init() {
super.init()
self.fillColor = UIColor.white.cgColor
self.fillRule = .evenOdd
self.fillColor = nil
self.strokeColor = UIColor.white.cgColor
self.lineWidth = 2.0
self.lineCap = .round
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, lineWidth: CGFloat) {
let params = Params(
size: size,
lineWidth: lineWidth
)
if self.currentParams == params {
return
}
self.currentParams = params
let lineWidth: CGFloat = 2.0
let path = CGMutablePath()
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: size.width * 0.5 - lineWidth * 0.5, startAngle: 0.0, endAngle: CGFloat.pi * 0.25, clockwise: false)
self.path = path
if self.animation(forKey: "rotation") == nil {
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.duration = 2.0
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.add(basicAnimation, forKey: "rotation")
}
}
}
public final class StoryPeerListItemComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let peer: EnginePeer
public let hasUnseen: Bool
public let hasItems: Bool
public let progress: CGFloat?
public let collapseFraction: CGFloat
public let collapsedWidth: CGFloat
public let leftNeighborDistance: CGFloat?
public let rightNeighborDistance: CGFloat?
public let action: (EnginePeer) -> Void
public init(
@ -24,6 +149,12 @@ public final class StoryPeerListItemComponent: Component {
strings: PresentationStrings,
peer: EnginePeer,
hasUnseen: Bool,
hasItems: Bool,
progress: CGFloat?,
collapseFraction: CGFloat,
collapsedWidth: CGFloat,
leftNeighborDistance: CGFloat?,
rightNeighborDistance: CGFloat?,
action: @escaping (EnginePeer) -> Void
) {
self.context = context
@ -31,6 +162,12 @@ public final class StoryPeerListItemComponent: Component {
self.strings = strings
self.peer = peer
self.hasUnseen = hasUnseen
self.hasItems = hasItems
self.progress = progress
self.collapseFraction = collapseFraction
self.collapsedWidth = collapsedWidth
self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance
self.action = action
}
@ -50,24 +187,70 @@ public final class StoryPeerListItemComponent: Component {
if lhs.hasUnseen != rhs.hasUnseen {
return false
}
if lhs.hasItems != rhs.hasItems {
return false
}
if lhs.progress != rhs.progress {
return false
}
if lhs.collapseFraction != rhs.collapseFraction {
return false
}
if lhs.collapsedWidth != rhs.collapsedWidth {
return false
}
if lhs.leftNeighborDistance != rhs.leftNeighborDistance {
return false
}
if lhs.rightNeighborDistance != rhs.rightNeighborDistance {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private let avatarContainer: UIView
private var avatarNode: AvatarNode?
private let indicatorCircleView: UIImageView
private var avatarAddBadgeView: UIImageView?
private let avatarShapeLayer: SimpleShapeLayer
private let indicatorMaskLayer: SimpleLayer
private let indicatorColorLayer: SimpleGradientLayer
private var progressLayer: StoryProgressLayer?
private let indicatorShapeLayer: SimpleShapeLayer
private let title = ComponentView<Empty>()
private var component: StoryPeerListItemComponent?
private weak var componentState: EmptyComponentState?
public override init(frame: CGRect) {
self.indicatorCircleView = UIImageView()
self.indicatorCircleView.isUserInteractionEnabled = false
self.avatarContainer = UIView()
self.avatarContainer.isUserInteractionEnabled = false
self.avatarShapeLayer = SimpleShapeLayer()
self.indicatorColorLayer = SimpleGradientLayer()
self.indicatorColorLayer.type = .axial
self.indicatorColorLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.indicatorColorLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.indicatorMaskLayer = SimpleLayer()
self.indicatorShapeLayer = SimpleShapeLayer()
super.init(frame: frame)
self.addSubview(self.indicatorCircleView)
self.addSubview(self.avatarContainer)
self.layer.addSublayer(self.indicatorColorLayer)
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
self.indicatorColorLayer.mask = self.indicatorMaskLayer
self.avatarShapeLayer.fillColor = UIColor.white.cgColor
self.avatarShapeLayer.fillRule = .evenOdd
self.indicatorShapeLayer.fillColor = nil
self.indicatorShapeLayer.strokeColor = UIColor.white.cgColor
self.indicatorShapeLayer.lineWidth = 2.0
self.indicatorShapeLayer.lineCap = .round
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
@ -100,70 +283,159 @@ public final class StoryPeerListItemComponent: Component {
}
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let hadUnseen = self.component?.hasUnseen ?? false
let hadUnseen = self.component?.hasUnseen
let hadProgress = self.component?.progress != nil
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.componentState = state
let effectiveWidth: CGFloat = (1.0 - component.collapseFraction) * availableSize.width + component.collapseFraction * component.collapsedWidth
let effectiveScale: CGFloat = 1.0 * (1.0 - component.collapseFraction) + (24.0 / 52.0) * component.collapseFraction
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.avatarNode = avatarNode
avatarNode.layer.mask = self.avatarShapeLayer
avatarNode.isUserInteractionEnabled = false
self.addSubview(avatarNode.view)
self.avatarContainer.addSubview(avatarNode.view)
}
let avatarSize = CGSize(width: 52.0, height: 52.0)
let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5), y: 4.0), size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: 4.0), size: avatarSize)
transition.setFrame(view: avatarNode.view, frame: CGRect(origin: CGPoint(), size: avatarFrame.size))
let indicatorFrame = avatarFrame.insetBy(dx: -4.0, dy: -4.0)
let indicatorLineWidth: CGFloat = 2.0 * (1.0 - component.collapseFraction) + (1.33 * (1.0 / effectiveScale)) * (component.collapseFraction)
avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer
)
avatarNode.updateSize(size: avatarSize)
transition.setFrame(view: avatarNode.view, frame: avatarFrame)
transition.setPosition(view: self.avatarContainer, position: avatarFrame.center)
transition.setBounds(view: self.avatarContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
if component.peer.id == component.context.account.peerId && !component.hasUnseen {
self.indicatorCircleView.image = nil
} else if self.indicatorCircleView.image == nil || hadUnseen != component.hasUnseen {
self.indicatorCircleView.image = generateImage(indicatorFrame.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let lineWidth: CGFloat = 2.0
context.setLineWidth(lineWidth)
context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
context.replacePathWithStrokedPath()
context.clip()
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor]
if component.hasUnseen {
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
let scaledAvatarSize = effectiveScale * (avatarSize.width + 4.0 - indicatorLineWidth * 2.0)
transition.setScale(view: self.avatarContainer, scale: scaledAvatarSize / avatarSize.width)
if component.peer.id == component.context.account.peerId && !component.hasItems {
self.indicatorColorLayer.isHidden = true
let avatarAddBadgeView: UIImageView
var avatarAddBadgeTransition = transition
if let current = self.avatarAddBadgeView {
avatarAddBadgeView = current
} else {
avatarAddBadgeTransition = .immediate
avatarAddBadgeView = UIImageView()
self.avatarAddBadgeView = avatarAddBadgeView
self.avatarContainer.addSubview(avatarAddBadgeView)
}
let badgeSize = CGSize(width: 16.0, height: 16.0)
if avatarAddBadgeView.image == nil || themeUpdated {
avatarAddBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(component.theme.list.itemCheckColors.fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(component.theme.list.itemCheckColors.foregroundColor.cgColor)
context.setLineWidth(UIScreenPixel * 3.0)
context.setLineCap(.round)
let lineSize: CGFloat = 9.0 + UIScreenPixel
context.move(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5))
context.addLine(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5 + lineSize))
context.strokePath()
context.move(to: CGPoint(x: (size.width - lineSize) * 0.5, y: size.height * 0.5))
context.addLine(to: CGPoint(x: (size.width - lineSize) * 0.5 + lineSize, y: size.height * 0.5))
context.strokePath()
})
}
avatarAddBadgeTransition.setFrame(view: avatarAddBadgeView, frame: CGRect(origin: CGPoint(x: avatarFrame.width - 1.0 - badgeSize.width, y: avatarFrame.height - 1.0 - badgeSize.height), size: badgeSize))
} else {
if indicatorColorLayer.isHidden {
self.indicatorColorLayer.isHidden = false
}
if let avatarAddBadgeView = self.avatarAddBadgeView {
self.avatarAddBadgeView = nil
avatarAddBadgeView.removeFromSuperview()
}
}
transition.setFrame(view: self.indicatorCircleView, frame: indicatorFrame)
if hadUnseen != component.hasUnseen || hadProgress != (component.progress != nil) {
let locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor]
if component.hasUnseen || component.progress != nil {
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
self.indicatorColorLayer.locations = locations.map { $0 as NSNumber }
self.indicatorColorLayer.colors = colors
}
transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.center)
transition.setBounds(layer: self.indicatorColorLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
transition.setPosition(layer: self.indicatorShapeLayer, position: CGPoint(x: indicatorFrame.width * 0.5, y: indicatorFrame.height * 0.5))
transition.setBounds(layer: self.indicatorShapeLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
transition.setScale(layer: self.indicatorColorLayer, scale: effectiveScale)
let indicatorCenter = CGRect(origin: CGPoint(), size: indicatorFrame.size).center
var mappedLeftCenter: CGPoint?
var mappedRightCenter: CGPoint?
if let leftNeighborDistance = component.leftNeighborDistance {
mappedLeftCenter = CGPoint(x: indicatorCenter.x - leftNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y)
}
if let rightNeighborDistance = component.rightNeighborDistance {
mappedRightCenter = CGPoint(x: indicatorCenter.x + rightNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y)
}
let avatarPath = CGMutablePath()
avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -1.0, dy: -1.0))
if component.peer.id == component.context.account.peerId && !component.hasItems {
let cutoutSize: CGFloat = 18.0 + UIScreenPixel * 2.0
avatarPath.addEllipse(in: CGRect(origin: CGPoint(x: avatarSize.width - cutoutSize + UIScreenPixel, y: avatarSize.height - cutoutSize + UIScreenPixel), size: CGSize(width: cutoutSize, height: cutoutSize)))
} else if let mappedRightCenter {
avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineWidth, dy: -indicatorLineWidth).offsetBy(dx: abs(mappedRightCenter.x - indicatorCenter.x), dy: 0.0))
}
self.avatarShapeLayer.path = avatarPath
self.indicatorShapeLayer.path = calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorFrame.width * 0.5 - indicatorLineWidth * 0.5)
//TODO:localize
let titleString: String
if component.peer.id == component.context.account.peerId {
if let _ = component.progress {
titleString = "Uploading..."
} else {
titleString = "My story"
}
} else {
titleString = component.peer.compactDisplayTitle
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.peer.id == component.context.account.peerId ? "My story" : component.peer.compactDisplayTitle, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
component: AnyComponent(Text(text: titleString, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
environment: {},
containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: indicatorFrame.maxY + 3.0), size: titleSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.25, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 3.0) * effectiveScale), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
@ -171,7 +443,40 @@ public final class StoryPeerListItemComponent: Component {
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setScale(view: titleView, scale: effectiveScale)
transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction)
}
if component.progress != nil {
var progressTransition = transition
let progressLayer: StoryProgressLayer
if let current = self.progressLayer {
progressLayer = current
} else {
progressTransition = .immediate
progressLayer = StoryProgressLayer()
self.progressLayer = progressLayer
self.indicatorMaskLayer.addSublayer(progressLayer)
}
let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size)
progressTransition.setFrame(layer: progressLayer, frame: progressFrame)
progressLayer.update(size: progressFrame.size, lineWidth: 4.0)
self.indicatorShapeLayer.opacity = 0.0
} else {
self.indicatorShapeLayer.opacity = 1.0
if let progressLayer = self.progressLayer {
self.progressLayer = nil
if transition.animation.isImmediate {
progressLayer.removeFromSuperlayer()
} else {
progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in
progressLayer?.removeFromSuperlayer()
})
}
}
}
return availableSize

View File

@ -22,6 +22,7 @@ import LegacyComponents
import LegacyMediaPickerUI
import LegacyCamera
import AvatarNode
import LocalMediaResources
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData
@ -61,7 +62,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh
}
}
public final class TelegramRootController: NavigationController {
public final class TelegramRootController: NavigationController, TelegramRootControllerInterface {
private let context: AccountContext
public var rootTabController: TabBarController?
@ -270,7 +271,7 @@ public final class TelegramRootController: NavigationController {
item = TGMediaAsset(phAsset: asset)
}
let context = self.context
legacyStoryMediaEditor(context: self.context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in
legacyStoryMediaEditor(context: context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in
dismissCameraImpl?()
guard let self else {
@ -362,20 +363,49 @@ public final class TelegramRootController: NavigationController {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
if let data, let image = UIImage(data: data) {
Queue.mainQueue().async {
let _ = (context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy)
|> deliverOnMainQueue).start(completed: {
switch asset.mediaType {
case .image:
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
if let data, let image = UIImage(data: data) {
Queue.mainQueue().async {
guard let self else {
return
}
let _ = self
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
storyListContext.upload(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy)
}
selectionController?.dismiss()
})
/*let _ = (context.engine.messages.uploadStory(media: )
|> deliverOnMainQueue).start(completed: {
guard let self else {
return
}
let _ = self
selectionController?.dismiss()
})*/
}
}
})
case .video:
let resource = VideoLibraryMediaResource(localIdentifier: asset.localIdentifier, conversion: VideoLibraryMediaResourceConversion.passthrough)
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
storyListContext.upload(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), privacy: privacy)
}
})
selectionController?.dismiss()
/*let _ = (context.engine.messages.uploadStory(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), privacy: privacy)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
let _ = self
selectionController?.dismiss()
})*/
default:
selectionController?.dismiss()
}
}
})
}, present: { c, a in