diff --git a/submodules/TelegramCore/Sources/Network/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift index 3bf92c09da..d695966a5b 100644 --- a/submodules/TelegramCore/Sources/Network/Download.swift +++ b/submodules/TelegramCore/Sources/Network/Download.swift @@ -117,7 +117,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data)) } - return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true) + return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true) |> mapError { error -> UploadPartError in if error.errorCode == 400 { return .invalidMedia diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index 652ee0df72..cd56b068fe 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -453,6 +453,7 @@ private final class FetchImpl { let reuploadSignal = self.network.multiplexedRequestManager.request( to: .main(state.cdnData.sourceDatacenterId), consumerId: self.consumerId, + resourceId: self.resource.id.stringRepresentation, data: Api.functions.upload.reuploadCdnFile( fileToken: Buffer(data: state.cdnData.fileToken), requestToken: Buffer(data: state.refreshToken) @@ -565,6 +566,7 @@ private final class FetchImpl { filePartRequest = self.network.multiplexedRequestManager.request( to: .cdn(cdnData.id), consumerId: self.consumerId, + resourceId: self.resource.id.stringRepresentation, data: Api.functions.upload.getCdnFile( fileToken: Buffer(data: cdnData.fileToken), offset: requestedOffset, @@ -608,6 +610,7 @@ private final class FetchImpl { filePartRequest = self.network.multiplexedRequestManager.request( to: .main(sourceDatacenterId), consumerId: self.consumerId, + resourceId: self.resource.id.stringRepresentation, data: Api.functions.upload.getFile( flags: 0, location: inputLocation, diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index 9666c4d79e..c201c679f1 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -81,11 +81,28 @@ private enum MultipartFetchMasterLocation { private struct DownloadWrapper { let consumerId: Int64 + let resourceId: String? let datacenterId: Int32 let isCdn: Bool let network: Network let useMainConnection: Bool + init( + consumerId: Int64, + resourceId: String?, + datacenterId: Int32, + isCdn: Bool, + network: Network, + useMainConnection: Bool + ) { + self.consumerId = consumerId + self.resourceId = resourceId + self.datacenterId = datacenterId + self.isCdn = isCdn + self.network = network + self.useMainConnection = useMainConnection + } + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool) -> Signal<(T, NetworkResponseInfo), MTRpcError> { let target: MultiplexedRequestTarget if self.isCdn { @@ -93,7 +110,7 @@ private struct DownloadWrapper { } else { target = .main(Int(self.datacenterId)) } - return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, data: data, tag: tag, continueInBackground: continueInBackground) + return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground) |> mapError { error, _ -> MTRpcError in return error } @@ -581,7 +598,7 @@ private final class MultipartFetchManager { self.network = network self.networkStatsContext = networkStatsContext self.revalidationContext = revalidationContext - self.source = .master(location: location, download: DownloadWrapper(consumerId: self.consumerId, datacenterId: location.datacenterId, isCdn: false, network: network, useMainConnection: self.useMainConnection)) + self.source = .master(location: location, download: DownloadWrapper(consumerId: self.consumerId, resourceId: self.resource.id.stringRepresentation, datacenterId: location.datacenterId, isCdn: false, network: network, useMainConnection: self.useMainConnection)) self.partReady = partReady self.reportCompleteSize = reportCompleteSize self.finishWithError = finishWithError @@ -884,7 +901,7 @@ private final class MultipartFetchManager { switch strongSelf.source { case let .master(location, download): strongSelf.partAlignment = dataHashLength - strongSelf.source = .cdn(masterDatacenterId: location.datacenterId, cdnDatacenterId: id, fileToken: token, key: key, iv: iv, download: DownloadWrapper(consumerId: strongSelf.consumerId, datacenterId: id, isCdn: true, network: strongSelf.network, useMainConnection: strongSelf.useMainConnection), masterDownload: download, hashSource: MultipartCdnHashSource(queue: strongSelf.queue, fileToken: token, hashes: partHashes, masterDownload: download, continueInBackground: strongSelf.continueInBackground)) + strongSelf.source = .cdn(masterDatacenterId: location.datacenterId, cdnDatacenterId: id, fileToken: token, key: key, iv: iv, download: DownloadWrapper(consumerId: strongSelf.consumerId, resourceId: strongSelf.resource.id.stringRepresentation, datacenterId: id, isCdn: true, network: strongSelf.network, useMainConnection: strongSelf.useMainConnection), masterDownload: download, hashSource: MultipartCdnHashSource(queue: strongSelf.queue, fileToken: token, hashes: partHashes, masterDownload: download, continueInBackground: strongSelf.continueInBackground)) strongSelf.checkState() case .cdn, .none: break diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift index b628437270..e0929a3d47 100644 --- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift +++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift @@ -26,6 +26,7 @@ private struct MultiplexedRequestTargetKey: Equatable, Hashable { private final class RequestData { let id: Int32 let consumerId: Int64 + let resourceId: String? let target: MultiplexedRequestTarget let functionDescription: FunctionDescription let payload: Buffer @@ -36,9 +37,10 @@ private final class RequestData { let completed: (Any, NetworkResponseInfo) -> Void let error: (MTRpcError, Double) -> Void - init(id: Int32, consumerId: Int64, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { + init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { self.id = id self.consumerId = consumerId + self.resourceId = resourceId self.target = target self.functionDescription = functionDescription self.tag = tag @@ -86,11 +88,11 @@ struct NetworkResponseInfo { var networkDuration: Double } -final class RequestManagerPriorityContext { - -} - private final class MultiplexedRequestManagerContext { + final class RequestManagerPriorityContext { + var resourceCounters: [String: Int] = [:] + } + private let queue: Queue private let takeWorker: (MultiplexedRequestTarget, MediaResourceFetchTag?, Bool) -> Download? @@ -119,12 +121,29 @@ private final class MultiplexedRequestManagerContext { } } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { + func pushPriority(resourceId: String) -> Disposable { + let queue = self.queue + + let value = self.priorityContext.resourceCounters[resourceId] ?? 0 + self.priorityContext.resourceCounters[resourceId] = value + 1 + + return ActionDisposable { [weak self] in + queue.async { + guard let `self` = self else { + return + } + let value = self.priorityContext.resourceCounters[resourceId] ?? 0 + self.priorityContext.resourceCounters[resourceId] = max(0, value - 1) + } + } + } + + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { let targetKey = MultiplexedRequestTargetKey(target: target, continueInBackground: continueInBackground) let requestId = self.nextId self.nextId += 1 - self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, deserializeResponse: { buffer in + self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, deserializeResponse: { buffer in return data.2(buffer) }, completed: { result, info in completed(result, info) @@ -169,6 +188,29 @@ private final class MultiplexedRequestManagerContext { let maxWorkersPerTarget = 4 for request in self.queuedRequests.sorted(by: { lhs, rhs in + let lhsPriority = lhs.resourceId.flatMap { id in + if let counters = self.priorityContext.resourceCounters[id], counters > 0 { + return true + } else { + return false + } + } ?? false + let rhsPriority = rhs.resourceId.flatMap { id in + if let counters = self.priorityContext.resourceCounters[id], counters > 0 { + return true + } else { + return false + } + } ?? false + + if lhsPriority != rhsPriority { + if lhsPriority { + return true + } else { + return false + } + } + return lhs.id < rhs.id }) { let targetKey = MultiplexedRequestTargetKey(target: request.target, continueInBackground: request.continueInBackground) @@ -291,11 +333,19 @@ final class MultiplexedRequestManager { }) } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal { + func pushPriority(resourceId: String) -> Disposable { + let disposable = MetaDisposable() + self.context.with { context in + disposable.set(context.pushPriority(resourceId: resourceId)) + } + return disposable + } + + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in - disposable.set(context.request(to: target, consumerId: consumerId, data: (data.0, data.1, { buffer in + disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, completed: { result, _ in if let result = result as? T { @@ -312,11 +362,11 @@ final class MultiplexedRequestManager { } } - func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { + func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in - disposable.set(context.request(to: target, consumerId: consumerId, data: (data.0, data.1, { buffer in + disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, completed: { result, info in if let result = result as? T { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index 946e6c83a6..91ef9b2342 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -398,5 +398,9 @@ public extension TelegramEngine { public func cancelAllFetches(id: String) { preconditionFailure() } + + public func pushPriorityDownload(resourceId: String) -> Disposable { + return self.account.network.multiplexedRequestManager.pushPriority(resourceId: resourceId) + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 2a048f019a..80c4f85dea 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1153,13 +1153,15 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { let item = state.items[focusedIndex] self.focusedId = item.id - let allItems = state.items.map { stateItem -> StoryContentItem in - return StoryContentItem( - position: nil, + var allItems: [StoryContentItem] = [] + for i in 0 ..< state.items.count { + let stateItem = state.items[i] + allItems.append(StoryContentItem( + position: i, peerId: peer.id, storyItem: stateItem, entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles) - ) + )) } stateValue = StoryContentContextState( @@ -1167,7 +1169,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { peer: peer, additionalPeerData: additionalPeerData, item: StoryContentItem( - position: nil, + position: focusedIndex, peerId: peer.id, storyItem: item, entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 16ba58f471..8f4d0de372 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -58,6 +58,7 @@ final class StoryItemContentComponent: Component { private var currentMessageMedia: EngineMedia? private var fetchDisposable: Disposable? + private var priorityDisposable: Disposable? private var component: StoryItemContentComponent? private weak var state: EmptyComponentState? @@ -105,6 +106,7 @@ final class StoryItemContentComponent: Component { deinit { self.fetchDisposable?.dispose() + self.priorityDisposable?.dispose() self.currentProgressTimer?.invalidate() self.videoProgressDisposable?.dispose() } @@ -433,11 +435,18 @@ final class StoryItemContentComponent: Component { } if reloadMedia, let messageMedia, let peerReference { + self.priorityDisposable?.dispose() + self.priorityDisposable = nil + var fetchSignal: Signal? switch messageMedia { - case .image: - break + case let .image(image): + if let representation = largestImageRepresentation(image.representations) { + self.priorityDisposable = component.context.engine.resources.pushPriorityDownload(resourceId: representation.resource.id.stringRepresentation) + } case let .file(file): + self.priorityDisposable = component.context.engine.resources.pushPriorityDownload(resourceId: file.resource.id.stringRepresentation) + fetchSignal = fetchedMediaResource( mediaBox: component.context.account.postbox.mediaBox, userLocation: .other, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index bc0a3602dd..b268810ead 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -42,12 +42,20 @@ final class StoryItemImageView: UIView { func update(context: AccountContext, peer: EnginePeer, storyId: Int32, media: EngineMedia, size: CGSize, isCaptureProtected: Bool, attemptSynchronous: Bool, transition: Transition) { var dimensions: CGSize? + + let isMediaUpdated: Bool + if let currentMedia = self.currentMedia { + isMediaUpdated = !currentMedia._asMedia().isSemanticallyEqual(to: media._asMedia()) + } else { + isMediaUpdated = true + } + switch media { case let .image(image): if let representation = largestImageRepresentation(image.representations) { dimensions = representation.dimensions.cgSize - if self.currentMedia != media { + if isMediaUpdated { if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() { @@ -104,7 +112,7 @@ final class StoryItemImageView: UIView { case let .file(file): dimensions = file.dimensions?.cgSize - if self.currentMedia != media { + if isMediaUpdated { let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation()) if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 543d65ce51..4370a19614 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -227,16 +227,33 @@ public final class StoryItemSetContainerComponent: Component { struct ItemLayout { var containerSize: CGSize var contentFrame: CGRect - var contentVisualScale: CGFloat + var contentMinScale: CGFloat + var contentScaleFraction: CGFloat + + var itemSpacing: CGFloat + var centralVisibleItemWidth: CGFloat + var sideVisibleItemWidth: CGFloat + var fullItemScrollDistance: CGFloat + var sideVisibleItemScale: CGFloat + var halfItemScrollDistance: CGFloat init( containerSize: CGSize, contentFrame: CGRect, - contentVisualScale: CGFloat + contentMinScale: CGFloat, + contentScaleFraction: CGFloat ) { self.containerSize = containerSize self.contentFrame = contentFrame - self.contentVisualScale = contentVisualScale + self.contentMinScale = contentMinScale + self.contentScaleFraction = contentScaleFraction + + self.itemSpacing = 12.0 + self.centralVisibleItemWidth = self.contentFrame.width * self.contentMinScale + self.sideVisibleItemWidth = self.centralVisibleItemWidth - 30.0 + self.fullItemScrollDistance = self.centralVisibleItemWidth * 0.5 + self.itemSpacing + self.sideVisibleItemWidth * 0.5 + self.sideVisibleItemScale = self.contentMinScale * (self.sideVisibleItemWidth / self.centralVisibleItemWidth) + self.halfItemScrollDistance = self.sideVisibleItemWidth * 0.5 + self.itemSpacing + self.sideVisibleItemWidth * 0.5 } } @@ -351,8 +368,6 @@ public final class StoryItemSetContainerComponent: Component { var visibleItems: [Int32: VisibleItem] = [:] var trulyValidIds: [Int32] = [] - var scrollingOffsetX: CGFloat = 0.0 - var scrollingCenterX: CGFloat = 0.0 var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? @@ -392,7 +407,7 @@ public final class StoryItemSetContainerComponent: Component { self.scroller.alwaysBounceHorizontal = true self.scroller.showsVerticalScrollIndicator = false self.scroller.showsHorizontalScrollIndicator = false - self.scroller.decelerationRate = .fast + self.scroller.decelerationRate = .normal self.scroller.delaysContentTouches = false self.controlsContainerView = SparseContainerView() @@ -803,14 +818,33 @@ public final class StoryItemSetContainerComponent: Component { public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.scrollingOffsetX = scrollView.contentOffset.x - - self.adjustScroller() self.updateScrolling(transition: .immediate) + + if let component = self.component, let itemLayout = self.itemLayout, itemLayout.contentScaleFraction >= 1.0 - 0.0001 { + var index = Int(round(scrollView.contentOffset.x / itemLayout.fullItemScrollDistance)) + index = max(0, min(index, component.slice.allItems.count - 1)) + + if let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if index != currentIndex { + let nextId = component.slice.allItems[index].storyItem.id + self.awaitingSwitchToId = (component.slice.allItems[currentIndex].storyItem.id, nextId) + component.navigate(.id(nextId)) + } + } + } } } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let itemLayout = self.itemLayout else { + return + } + if targetContentOffset.pointee.x <= 0.0 || targetContentOffset.pointee.x >= scrollView.contentSize.width - scrollView.bounds.width { + return + } + + let closestIndex = Int(round(targetContentOffset.pointee.x / itemLayout.fullItemScrollDistance)) + targetContentOffset.pointee.x = CGFloat(closestIndex) * itemLayout.fullItemScrollDistance } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { @@ -824,86 +858,16 @@ public final class StoryItemSetContainerComponent: Component { } private func snapScrolling() { - self.scroller.setContentOffset(CGPoint(x: self.scrollingCenterX, y: 0.0), animated: true) - } - - private func adjustScroller() { - guard let component = self.component, let itemLayout = self.itemLayout else { + guard let itemLayout = self.itemLayout else { + return + } + let contentOffset = self.scroller.contentOffset + if contentOffset.x <= 0.0 || contentOffset.x >= self.scroller.contentSize.width - self.scroller.bounds.width { return } - self.ignoreScrolling = true - - self.scroller.isScrollEnabled = self.displayViewList - - let itemSpacing: CGFloat = 12.0 - let centralVisibleItemWidth = itemLayout.contentFrame.width * itemLayout.contentVisualScale - let sideVisibleItemWidth = centralVisibleItemWidth - 30.0 - let fullItemScrollDistance = centralVisibleItemWidth * 0.5 + itemSpacing + sideVisibleItemWidth * 0.5 - - var additionalInitializationDistance: CGFloat = 0.0 - if let (switchFromId, switchToId) = self.awaitingSwitchToId { - if component.slice.item.storyItem.id == switchToId { - self.awaitingSwitchToId = nil - - if let previousIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == switchFromId }), let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == switchToId }) { - let fractionDistance = CGFloat(previousIndex - centralIndex) - - let currentOffset = self.scrollingCenterX - self.scrollingOffsetX - additionalInitializationDistance = -(currentOffset - fractionDistance * fullItemScrollDistance) - } - - self.initializedOffset = false - } else { - self.ignoreScrolling = false - return - } - } - - if let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { - var leftWidth: CGFloat = 0.0 - var rightWidth: CGFloat = 0.0 - if centralIndex != 0 { - leftWidth = 600.0 - } - if centralIndex != component.slice.allItems.count - 1 { - rightWidth = 600.0 - } - - self.scrollingCenterX = leftWidth - self.scroller.contentSize = CGSize(width: leftWidth + itemLayout.containerSize.width + rightWidth, height: 1.0) - - if !self.initializedOffset { - self.initializedOffset = true - self.scrollingOffsetX = leftWidth + additionalInitializationDistance - self.scroller.contentOffset = CGPoint(x: self.scrollingOffsetX, y: 0.0) - } - - var lowestFraction: (Int, CGFloat)? - - for index in 0 ..< component.slice.allItems.count { - let offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance - let centerFraction: CGFloat = CGFloat(index - centralIndex) - - let combinedFraction = abs(offsetFraction + centerFraction) - - if let (_, lowestValue) = lowestFraction { - if combinedFraction < lowestValue { - lowestFraction = (index, combinedFraction) - } - } else { - lowestFraction = (index, combinedFraction) - } - } - - if let (index, _) = lowestFraction, index != centralIndex { - let fixedId = component.slice.allItems[index].storyItem.id - component.navigate(.id(fixedId)) - self.awaitingSwitchToId = (component.slice.item.storyItem.id, fixedId) - } - } - - self.ignoreScrolling = false + let closestIndex = Int(round(contentOffset.x / itemLayout.fullItemScrollDistance)) + self.scroller.setContentOffset(CGPoint(x: CGFloat(closestIndex) * itemLayout.fullItemScrollDistance, y: 0.0), animated: true) } private func isProgressPaused() -> Bool { @@ -957,43 +921,50 @@ public final class StoryItemSetContainerComponent: Component { var validIds: [Int32] = [] var trulyValidIds: [Int32] = [] - let centralItemFrame = itemLayout.contentFrame.center.offsetBy(dx: 0.0, dy: 0.0) + let centralItemX = itemLayout.contentFrame.center.x - let centralVisibleItemWidth = itemLayout.contentFrame.width * itemLayout.contentVisualScale - let sideVisibleItemWidth = centralVisibleItemWidth - 30.0 - let sideVisibleItemScale = itemLayout.contentVisualScale * (sideVisibleItemWidth / centralVisibleItemWidth) - - let itemSpacing: CGFloat = 12.0 - - let fullItemScrollDistance = centralVisibleItemWidth * 0.5 + itemSpacing + sideVisibleItemWidth * 0.5 - let halfItemScrollDistance = sideVisibleItemWidth * 0.5 + itemSpacing + sideVisibleItemWidth * 0.5 + let currentContentScale = itemLayout.contentMinScale * itemLayout.contentScaleFraction + 1.0 * (1.0 - itemLayout.contentScaleFraction) + let scaledCentralVisibleItemWidth = itemLayout.contentFrame.width * currentContentScale + let scaledSideVisibleItemWidth = scaledCentralVisibleItemWidth - 30.0 * itemLayout.contentScaleFraction + let scaledFullItemScrollDistance = scaledCentralVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 + let scaledHalfItemScrollDistance = scaledSideVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 if let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + let centralItemOffset: CGFloat = itemLayout.fullItemScrollDistance * CGFloat(centralIndex) + let effectiveScrollingOffsetX = self.scroller.contentOffset.x * itemLayout.contentScaleFraction + centralItemOffset * (1.0 - itemLayout.contentScaleFraction) + for index in 0 ..< component.slice.allItems.count { let item = component.slice.allItems[index] - var offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance + var offsetFraction: CGFloat = -effectiveScrollingOffsetX / itemLayout.fullItemScrollDistance if let viewListPanState = self.viewListPanState { offsetFraction += viewListPanState.fraction } - let centerIndexOffset = index - centralIndex - let centerFraction: CGFloat = CGFloat(centerIndexOffset) + let zeroIndexOffset = index + let centerFraction: CGFloat = CGFloat(zeroIndexOffset) let combinedFraction = offsetFraction + centerFraction let combinedFractionSign: CGFloat = combinedFraction < 0.0 ? -1.0 : 1.0 let fractionDistanceToCenter: CGFloat = min(1.0, abs(combinedFraction)) - var itemPosition = centralItemFrame - itemPosition.x += min(1.0, abs(combinedFraction)) * combinedFractionSign * fullItemScrollDistance - itemPosition.x += max(0.0, abs(combinedFraction) - 1.0) * combinedFractionSign * halfItemScrollDistance + var itemPositionX = centralItemX + itemPositionX += min(1.0, abs(combinedFraction)) * combinedFractionSign * scaledFullItemScrollDistance + itemPositionX += max(0.0, abs(combinedFraction) - 1.0) * combinedFractionSign * scaledHalfItemScrollDistance - var itemVisible = true - if abs(centerIndexOffset) > 2 { - itemVisible = false + var logicalItemPositionX = centralItemX + logicalItemPositionX += min(1.0, abs(combinedFraction)) * combinedFractionSign * itemLayout.fullItemScrollDistance + logicalItemPositionX += max(0.0, abs(combinedFraction) - 1.0) * combinedFractionSign * itemLayout.halfItemScrollDistance + + var itemVisible = false + let itemLeftEdge = logicalItemPositionX - itemLayout.fullItemScrollDistance * 0.5 + let itemRightEdge = logicalItemPositionX + itemLayout.fullItemScrollDistance * 0.5 + if itemRightEdge >= -itemLayout.containerSize.width && itemLeftEdge < itemLayout.containerSize.width * 2.0 { + itemVisible = true } - if itemLayout.contentVisualScale >= 1.0 - 0.001 && !self.preparingToDisplayViewList { + + if itemLayout.contentScaleFraction <= 0.0001 && !self.preparingToDisplayViewList { if index != centralIndex { itemVisible = false } @@ -1012,7 +983,9 @@ public final class StoryItemSetContainerComponent: Component { } let scaleFraction: CGFloat = abs(max(-1.0, min(1.0, combinedFraction))) - let itemScale = itemLayout.contentVisualScale * (1.0 - scaleFraction) + sideVisibleItemScale * scaleFraction + + let minItemScale = itemLayout.contentMinScale * (1.0 - scaleFraction) + itemLayout.sideVisibleItemScale * scaleFraction + let itemScale: CGFloat = itemLayout.contentScaleFraction * minItemScale + (1.0 - itemLayout.contentScaleFraction) * 1.0 validIds.append(item.storyItem.id) if itemVisible { @@ -1063,7 +1036,7 @@ public final class StoryItemSetContainerComponent: Component { ) let _ = visibleItem.view.update( transition: itemTransition.withUserData(StoryItemContentComponent.Hint( - synchronousLoad: index == centralIndex + synchronousLoad: index == centralIndex && itemLayout.contentScaleFraction <= 0.0001 )), component: AnyComponent(StoryItemContentComponent( context: component.context, @@ -1086,7 +1059,7 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) let itemId = item.storyItem.id - itemTransition.setPosition(view: visibleItem.contentContainerView, position: itemPosition, completion: { [weak self] _ in + itemTransition.setPosition(view: visibleItem.contentContainerView, position: CGPoint(x: itemPositionX, y: itemLayout.contentFrame.center.y), completion: { [weak self] _ in guard reevaluateVisibilityOnCompletion, let self else { return } @@ -1618,10 +1591,12 @@ public final class StoryItemSetContainerComponent: Component { self.initializedOffset = false } var itemsTransition = transition + var resetScrollingOffsetWithItemTransition = false if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id { self.animateNextNavigationId = nil self.viewListPanState = nil itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) + resetScrollingOffsetWithItemTransition = true } if self.topContentGradientLayer.colors == nil { @@ -1912,6 +1887,10 @@ public final class StoryItemSetContainerComponent: Component { inputPanelIsOverlay = true } + var minimizedBottomContentHeight: CGFloat = 0.0 + var maximizedBottomContentHeight: CGFloat = 0.0 + var minimizedBottomContentFraction: CGFloat = 0.0 + var validViewListIds: [Int32] = [] if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id] @@ -1985,6 +1964,7 @@ public final class StoryItemSetContainerComponent: Component { peerId: component.slice.peer.id, safeInsets: component.safeInsets, storyItem: item.storyItem, + minimizedContentHeight: 325.0, outerExpansionFraction: outerExpansionFraction, outerExpansionDirection: outerExpansionDirection, close: { [weak self] in @@ -2084,8 +2064,11 @@ public final class StoryItemSetContainerComponent: Component { } } if id == component.slice.item.storyItem.id { - viewListInset = viewList.externalState.effectiveHeight + viewListInset = viewList.externalState.minimizedHeight * viewList.externalState.minimizationFraction + viewList.externalState.defaultHeight * (1.0 - viewList.externalState.minimizationFraction) inputPanelBottomInset = viewListInset + minimizedBottomContentHeight = viewList.externalState.minimizedHeight + maximizedBottomContentHeight = viewList.externalState.defaultHeight + minimizedBottomContentFraction = viewList.externalState.minimizationFraction } } @@ -2123,22 +2106,41 @@ public final class StoryItemSetContainerComponent: Component { let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778)) let contentDefaultBottomInset: CGFloat = bottomContentInset - let contentSize = itemSize let contentVisualBottomInset: CGFloat = max(contentDefaultBottomInset, viewListInset) + let contentVisualMaxBottomInset: CGFloat = max(contentDefaultBottomInset, maximizedBottomContentHeight) + let contentVisualMinBottomInset: CGFloat = max(contentDefaultBottomInset, minimizedBottomContentHeight) + + let contentVisualMaxHeight = min(itemSize.height, availableSize.height - component.containerInsets.top - contentVisualMaxBottomInset) + let contentSize = CGSize(width: itemSize.width, height: contentVisualMaxHeight) + let contentVisualMinHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualMinBottomInset) + + let contentVisualHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualBottomInset) + + //contentScaleFraction = 1.0 -> contentVisualScale = contentMinScale + //contentScaleFraction = 0.0 -> contentVisualScale = 1.0 - var contentVisualHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualBottomInset) - if contentVisualHeight < contentSize.height && contentVisualHeight >= contentSize.height - 5 { - contentVisualHeight = contentSize.height - } let contentVisualScale = min(1.0, contentVisualHeight / contentSize.height) + let contentMaxScale = min(1.0, contentVisualMaxHeight / contentSize.height) + let contentMinScale = min(1.0, contentVisualMinHeight / contentSize.height) + + let contentScaleFraction: CGFloat + if abs(contentMaxScale - contentMinScale) < CGFloat.ulpOfOne { + contentScaleFraction = 0.0 + } else { + contentScaleFraction = 1.0 - (contentVisualScale - contentMinScale) / (contentMaxScale - contentMinScale) + } + let _ = minimizedBottomContentFraction + //print("contentScaleFraction: \(contentScaleFraction), minimizedBottomContentFraction: \(minimizedBottomContentFraction)") + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize) let itemLayout = ItemLayout( containerSize: availableSize, contentFrame: contentFrame, - contentVisualScale: contentVisualScale + contentMinScale: contentMinScale, + contentScaleFraction: contentScaleFraction ) self.itemLayout = itemLayout @@ -2899,10 +2901,28 @@ public final class StoryItemSetContainerComponent: Component { } self.ignoreScrolling = true + transition.setFrame(view: self.scroller, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.scroller.contentSize = CGSize(width: itemLayout.fullItemScrollDistance * CGFloat(max(0, component.slice.allItems.count - 1)) + availableSize.width, height: availableSize.height) + self.scroller.isScrollEnabled = itemLayout.contentScaleFraction >= 1.0 - 0.0001 + + if let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + let centralX = itemLayout.fullItemScrollDistance * CGFloat(centralIndex) + if itemLayout.contentScaleFraction <= 0.0001 { + if abs(self.scroller.contentOffset.x - centralX) > CGFloat.ulpOfOne { + self.scroller.contentOffset = CGPoint(x: centralX, y: 0.0) + } + } else if resetScrollingOffsetWithItemTransition { + let deltaX = centralX - self.scroller.contentOffset.x + if abs(deltaX) > CGFloat.ulpOfOne { + self.scroller.contentOffset = CGPoint(x: centralX, y: 0.0) + //itemsTransition.animateBoundsOrigin(view: self.itemsContainerView, from: CGPoint(x: deltaX, y: 0.0), to: CGPoint(), additive: true) + } + } + } + self.ignoreScrolling = false - self.adjustScroller() self.updateScrolling(transition: itemsTransition) if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 2dc6fe0b22..7f0a4da61f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -26,7 +26,8 @@ final class StoryItemSetViewListComponent: Component { final class ExternalState { fileprivate(set) var minimizedHeight: CGFloat = 0.0 - fileprivate(set) var effectiveHeight: CGFloat = 0.0 + fileprivate(set) var defaultHeight: CGFloat = 0.0 + fileprivate(set) var minimizationFraction: CGFloat = 0.0 init() { } @@ -47,6 +48,7 @@ final class StoryItemSetViewListComponent: Component { let peerId: EnginePeer.Id let safeInsets: UIEdgeInsets let storyItem: EngineStoryItem + let minimizedContentHeight: CGFloat let outerExpansionFraction: CGFloat let outerExpansionDirection: Bool let close: () -> Void @@ -64,6 +66,7 @@ final class StoryItemSetViewListComponent: Component { peerId: EnginePeer.Id, safeInsets: UIEdgeInsets, storyItem: EngineStoryItem, + minimizedContentHeight: CGFloat, outerExpansionFraction: CGFloat, outerExpansionDirection: Bool, close: @escaping () -> Void, @@ -80,6 +83,7 @@ final class StoryItemSetViewListComponent: Component { self.peerId = peerId self.safeInsets = safeInsets self.storyItem = storyItem + self.minimizedContentHeight = minimizedContentHeight self.outerExpansionFraction = outerExpansionFraction self.outerExpansionDirection = outerExpansionDirection self.close = close @@ -105,6 +109,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.storyItem != rhs.storyItem { return false } + if lhs.minimizedContentHeight != rhs.minimizedContentHeight { + return false + } if lhs.outerExpansionFraction != rhs.outerExpansionFraction { return false } @@ -559,7 +566,7 @@ final class StoryItemSetViewListComponent: Component { synchronous = animationHint.synchronous } - let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0)) + let minimizedHeight = max(100.0, availableSize.height - (component.minimizedContentHeight + 12.0)) if themeUpdated { self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor @@ -655,11 +662,17 @@ final class StoryItemSetViewListComponent: Component { let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset)) + var externalViews: EngineStoryItem.Views? = component.storyItem.views + if let viewListState = self.viewListState, !viewListState.items.isEmpty { + externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, seenPeers: viewListState.items.prefix(3).map(\.peer)) + } + let navigationPanelSize = self.navigationPanel.update( transition: transition, component: AnyComponent(StoryFooterPanelComponent( context: component.context, storyItem: component.storyItem, + externalViews: externalViews, expandFraction: dismissFraction, expandViewStats: { [weak self] in guard let self, let component = self.component else { @@ -853,9 +866,11 @@ final class StoryItemSetViewListComponent: Component { transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY)) component.externalState.minimizedHeight = minimizedHeight + component.externalState.defaultHeight = 60.0 + component.safeInsets.bottom + 1.0 - let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0) - component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight)) + //let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0) + //component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight)) + component.externalState.minimizationFraction = dismissFraction return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 22edbb0781..abe0f5deed 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -14,6 +14,7 @@ import SwiftSignalKit public final class StoryFooterPanelComponent: Component { public let context: AccountContext public let storyItem: EngineStoryItem? + public let externalViews: EngineStoryItem.Views? public let expandFraction: CGFloat public let expandViewStats: () -> Void public let deleteAction: () -> Void @@ -22,6 +23,7 @@ public final class StoryFooterPanelComponent: Component { public init( context: AccountContext, storyItem: EngineStoryItem?, + externalViews: EngineStoryItem.Views?, expandFraction: CGFloat, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, @@ -29,6 +31,7 @@ public final class StoryFooterPanelComponent: Component { ) { self.context = context self.storyItem = storyItem + self.externalViews = externalViews self.expandViewStats = expandViewStats self.expandFraction = expandFraction self.deleteAction = deleteAction @@ -42,6 +45,9 @@ public final class StoryFooterPanelComponent: Component { if lhs.storyItem != rhs.storyItem { return false } + if lhs.externalViews != rhs.externalViews { + return false + } if lhs.expandFraction != rhs.expandFraction { return false } @@ -233,7 +239,7 @@ public final class StoryFooterPanelComponent: Component { } var peers: [EnginePeer] = [] - if let seenPeers = component.storyItem?.views?.seenPeers { + if let seenPeers = component.externalViews?.seenPeers ?? component.storyItem?.views?.seenPeers { peers = Array(seenPeers.prefix(3)) } let avatarsContent = self.avatarsContext.update(peers: peers, animated: false) @@ -248,7 +254,7 @@ public final class StoryFooterPanelComponent: Component { } var viewCount = 0 - if let views = component.storyItem?.views, views.seenCount != 0 { + if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 { viewCount = views.seenCount }