From 1f0ac4aada9b1e9ff5f818a3d63a05c4c00e2cd3 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 31 Jul 2023 19:59:39 +0300 Subject: [PATCH 1/6] Make View Story button height-adaptive --- .../Sources/ChatMessageStoryMentionContentNode.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift index 5039ef1c52..b693a284f9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift @@ -196,7 +196,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_StoryMentionAction, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 186.0) + let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 167.0 + buttonTitleLayout.size.height) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in @@ -315,7 +315,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 19.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame - let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) + let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 15.0 + buttonTitleLayout.size.height) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 11.0), size: buttonSize) if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { From 27b39cc314bf55ae0db49fa777224681c2a13b85 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 31 Jul 2023 21:29:25 +0300 Subject: [PATCH 2/6] Fix open stories --- .../Stories/StoryContainerScreen/Sources/OpenStories.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index d6de013c55..386c949236 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -149,7 +149,10 @@ public extension StoryContainerScreen { guard let avatarNode else { return } - sharedProgressDisposable?.set(avatarNode.pushLoadingStatus(signal: signal)) + let disposable = avatarNode.pushLoadingStatus(signal: signal) + if let sharedProgressDisposable { + sharedProgressDisposable.set(disposable) + } } ) } From c88558ead861acd8adc350cfb26880d0f379795a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 1 Aug 2023 00:12:09 +0300 Subject: [PATCH 3/6] Fix child --- .../MediaEditorScreen/Sources/MediaEditorScreen.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c2e6982fc5..7c74ce49e7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4302,6 +4302,7 @@ final class DoneButtonContentComponent: CombinedComponent { let textSpacing: CGFloat = 8.0 var title: _UpdatedChildComponent? + var hideTitle = false if let titleText = context.component.title { title = text.update( component: Text( @@ -4317,7 +4318,7 @@ final class DoneButtonContentComponent: CombinedComponent { if updatedBackgroundWidth < 126.0 { backgroundSize.width = updatedBackgroundWidth } else { - title = nil + hideTitle = true } } @@ -4335,6 +4336,7 @@ final class DoneButtonContentComponent: CombinedComponent { if let title { context.add(title .position(CGPoint(x: title.size.width / 2.0 + 15.0, y: backgroundHeight / 2.0)) + .opacity(hideTitle ? 0.0 : 1.0) ) } From b6fe5dcc40b9af52816c455a0c71800bc69e837a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 1 Aug 2023 01:17:52 +0300 Subject: [PATCH 4/6] Story view list animations --- .../Sources/AnimatedCountLabelNode.swift | 244 ++++++++++++++++++ .../Sources/StoryItemContentComponent.swift | 34 ++- .../StoryItemSetContainerComponent.swift | 119 ++++++--- .../StoryItemSetViewListComponent.swift | 21 ++ .../Stories/StoryFooterPanelComponent/BUILD | 1 + .../Sources/StoryFooterPanelComponent.swift | 85 ++++-- 6 files changed, 440 insertions(+), 64 deletions(-) diff --git a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift index f34b9a1fa3..d6aa1afae1 100644 --- a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift +++ b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift @@ -288,3 +288,247 @@ public final class ImmediateAnimatedCountLabelNode: AnimatedCountLabelNode { return node } } + +public class AnimatedCountLabelView: UIView { + public struct Layout { + public var size: CGSize + public var isTruncated: Bool + } + + public enum Segment: Equatable { + case number(Int, NSAttributedString) + case text(Int, NSAttributedString) + + public static func ==(lhs: Segment, rhs: Segment) -> Bool { + switch lhs { + case let .number(number, text): + if case let .number(rhsNumber, rhsText) = rhs, number == rhsNumber, text.isEqual(to: rhsText) { + return true + } else { + return false + } + case let .text(index, text): + if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) { + return true + } else { + return false + } + } + } + } + + fileprivate enum ResolvedSegment: Equatable { + public enum Key: Hashable { + case number(Int) + case text(Int) + } + + case number(id: Int, value: Int, string: NSAttributedString) + case text(id: Int, string: NSAttributedString) + + public static func ==(lhs: ResolvedSegment, rhs: ResolvedSegment) -> Bool { + switch lhs { + case let .number(id, number, text): + if case let .number(rhsId, rhsNumber, rhsText) = rhs, id == rhsId, number == rhsNumber, text.isEqual(to: rhsText) { + return true + } else { + return false + } + case let .text(index, text): + if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) { + return true + } else { + return false + } + } + } + + public var attributedText: NSAttributedString { + switch self { + case let .number(_, _, text): + return text + case let .text(_, text): + return text + } + } + + var key: Key { + switch self { + case let .number(id, _, _): + return .number(id) + case let .text(index, _): + return .text(index) + } + } + } + + fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:] + + public var reverseAnimationDirection: Bool = false + public var alwaysOneDirection: Bool = false + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, segments initialSegments: [Segment], transition: ContainedViewLayoutTransition) -> Layout { + var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:] + let wasEmpty = self.resolvedSegments.isEmpty + for (segmentKey, segmentAndTextNode) in self.resolvedSegments { + segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1) + } + let reverseAnimationDirection = self.reverseAnimationDirection + let alwaysOneDirection = self.alwaysOneDirection + + var segments: [ResolvedSegment] = [] + loop: for segment in initialSegments { + switch segment { + case let .number(value, string): + if string.string.isEmpty { + continue loop + } + let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1)) + + var remainingValue = value + + let insertPosition = segments.count + + while true { + let digitValue = remainingValue % 10 + + segments.insert(.number(id: 1000 - segments.count, value: value, string: NSAttributedString(string: "\(digitValue)", attributes: attributes)), at: insertPosition) + remainingValue /= 10 + if remainingValue == 0 { + break + } + } + case let .text(id, string): + segments.append(.text(id: id, string: string)) + } + } + + for segment in segments { + if segmentLayouts[segment.key] == nil { + segmentLayouts[segment.key] = TextNode.asyncLayout(nil) + } + } + + var contentSize = CGSize() + var remainingSize = size + + var calculatedSegments: [ResolvedSegment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:] + var isTruncated = false + + var validKeys: [ResolvedSegment.Key] = [] + + for segment in segments { + validKeys.append(segment.key) + let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil)) + var effectiveSegmentWidth = layout.size.width + if case .number = segment { + //effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0 + } else if segment.attributedText.string == " " { + effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0) + } + calculatedSegments[segment.key] = (layout, effectiveSegmentWidth, apply) + contentSize.width += effectiveSegmentWidth + contentSize.height = max(contentSize.height, layout.size.height) + remainingSize.width = max(0.0, remainingSize.width - layout.size.width) + if layout.truncated { + isTruncated = true + } + } + + var transition = transition + if wasEmpty { + transition = .immediate + } + + var currentOffset = CGPoint() + for segment in segments { + var animation: (CGFloat, Double)? + if let (currentSegment, currentTextNode) = self.resolvedSegments[segment.key] { + if case let .number(_, currentValue, currentString) = currentSegment, case let .number(_, updatedValue, updatedString) = segment, transition.isAnimated, !wasEmpty, currentValue != updatedValue, currentString.string != updatedString.string, let snapshot = currentTextNode.layer.snapshotContentTree() { + var fromAlpha: CGFloat = 1.0 + if let presentation = currentTextNode.layer.presentation() { + fromAlpha = CGFloat(presentation.opacity) + } + var offsetY: CGFloat + if currentValue > updatedValue || alwaysOneDirection { + offsetY = -floor(currentTextNode.bounds.height * 0.6) + } else { + offsetY = floor(currentTextNode.bounds.height * 0.6) + } + if reverseAnimationDirection { + offsetY = -offsetY + } + animation = (-offsetY, 0.2) + snapshot.frame = currentTextNode.frame + self.layer.addSublayer(snapshot) + snapshot.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetY), duration: 0.2, removeOnCompletion: false, additive: true) + snapshot.animateScale(from: 1.0, to: 0.3, duration: 0.2, removeOnCompletion: false) + snapshot.animateAlpha(from: fromAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperlayer() + }) + } + } + + let (layout, effectiveSegmentWidth, apply) = calculatedSegments[segment.key]! + let textNode = apply() + let textFrame = CGRect(origin: currentOffset, size: layout.size) + if textNode.frame.isEmpty { + textNode.frame = textFrame + if transition.isAnimated, !wasEmpty, animation == nil { + textNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else if textNode.frame != textFrame { + transition.updateFrameAdditive(node: textNode, frame: textFrame) + } + currentOffset.x += effectiveSegmentWidth + if let (_, currentTextNode) = self.resolvedSegments[segment.key] { + if currentTextNode !== textNode { + currentTextNode.removeFromSupernode() + self.addSubnode(textNode) + } + } else { + textNode.displaysAsynchronously = false + textNode.isUserInteractionEnabled = false + self.addSubview(textNode.view) + } + if let (offset, duration) = animation { + textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, additive: true) + textNode.layer.animateScale(from: 0.3, to: 1.0, duration: duration) + textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } + self.resolvedSegments[segment.key] = (segment, textNode) + } + + var removeKeys: [ResolvedSegment.Key] = [] + for key in self.resolvedSegments.keys { + if !validKeys.contains(key) { + removeKeys.append(key) + } + } + + for key in removeKeys { + guard let (_, textNode) = self.resolvedSegments.removeValue(forKey: key) else { + continue + } + if transition.isAnimated { + textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in + textNode?.removeFromSupernode() + }) + } else { + textNode.removeFromSupernode() + } + } + + return Layout(size: contentSize, isTruncated: isTruncated) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 55736ddeaa..05a8222a14 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -32,14 +32,16 @@ final class StoryItemContentComponent: Component { let item: EngineStoryItem let audioMode: StoryContentItem.AudioMode let isVideoBuffering: Bool - - init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool) { + let isCurrent: Bool + + init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool) { self.context = context self.strings = strings self.peer = peer self.item = item self.audioMode = audioMode self.isVideoBuffering = isVideoBuffering + self.isCurrent = isCurrent } static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool { @@ -57,6 +59,9 @@ final class StoryItemContentComponent: Component { } if lhs.isVideoBuffering != rhs.isVideoBuffering { return false + } + if lhs.isCurrent != rhs.isCurrent { + return false } return true } @@ -94,6 +99,9 @@ final class StoryItemContentComponent: Component { private let hierarchyTrackingLayer: HierarchyTrackingLayer + private var fetchPriorityResourceId: String? + private var currentFetchPriority: (isMain: Bool, disposable: Disposable)? + override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.imageView = StoryItemImageView() @@ -121,6 +129,7 @@ final class StoryItemContentComponent: Component { self.priorityDisposable?.dispose() self.currentProgressTimer?.invalidate() self.videoProgressDisposable?.dispose() + self.currentFetchPriority?.disposable.dispose() } private func performActionAfterImageContentLoaded(update: Bool) { @@ -479,6 +488,27 @@ final class StoryItemContentComponent: Component { } } + var fetchPriorityResourceId: String? + switch messageMedia { + case let .image(image): + if let representation = largestImageRepresentation(image.representations) { + fetchPriorityResourceId = representation.resource.id.stringRepresentation + } + case let .file(file): + fetchPriorityResourceId = file.resource.id.stringRepresentation + default: + break + } + + if self.fetchPriorityResourceId != fetchPriorityResourceId || self.currentFetchPriority?.0 != component.isCurrent { + self.fetchPriorityResourceId = fetchPriorityResourceId + self.currentFetchPriority?.disposable.dispose() + + if let fetchPriorityResourceId { + self.currentFetchPriority = (component.isCurrent, component.context.engine.resources.pushPriorityDownload(resourceId: fetchPriorityResourceId, priority: component.isCurrent ? 2 : 1)) + } + } + if reloadMedia, let messageMedia, let peerReference { self.priorityDisposable?.dispose() self.priorityDisposable = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index c382d774e2..c41f20aa8b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -432,6 +432,7 @@ public final class StoryItemSetContainerComponent: Component { private var initializedOffset: Bool = false private var viewListPanState: PanState? + private var isCompletingViewListPan: Bool = false private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer? private var verticalPanState: PanState? @@ -838,6 +839,8 @@ public final class StoryItemSetContainerComponent: Component { let translation = recognizer.translation(in: self) let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width)) self.viewListPanState = PanState(fraction: fraction) + self.isCompletingViewListPan = false + self.layer.removeAnimation(forKey: "isCompletingViewListPan") self.state?.updated(transition: .immediate) } case .changed: @@ -846,6 +849,7 @@ public final class StoryItemSetContainerComponent: Component { let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width)) viewListPanState.fraction = fraction self.viewListPanState = viewListPanState + self.isCompletingViewListPan = false self.state?.updated(transition: .immediate) } case .cancelled, .ended: @@ -868,8 +872,19 @@ public final class StoryItemSetContainerComponent: Component { } if !consumed { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) self.viewListPanState = nil - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.isCompletingViewListPan = true + transition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in + guard let self, completed else { + return + } + if self.isCompletingViewListPan { + self.isCompletingViewListPan = false + self.state?.updated(transition: .immediate) + } + }) + self.state?.updated(transition: transition) } } default: @@ -1175,7 +1190,8 @@ public final class StoryItemSetContainerComponent: Component { peer: component.slice.peer, item: item.storyItem, audioMode: component.audioMode, - isVideoBuffering: visibleItem.isBuffering + isVideoBuffering: visibleItem.isBuffering, + isCurrent: index == centralIndex )), environment: { itemEnvironment @@ -1287,16 +1303,14 @@ public final class StoryItemSetContainerComponent: Component { return false } if component.slice.peer.id == component.context.account.peerId { - if let _ = component.slice.item.storyItem.views { - self.displayViewList = true - if component.verticalPanFraction == 0.0 { - self.preparingToDisplayViewList = true - self.updateScrolling(transition: .immediate) - self.preparingToDisplayViewList = false - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - return true + self.displayViewList = true + if component.verticalPanFraction == 0.0 { + self.preparingToDisplayViewList = true + self.updateScrolling(transition: .immediate) + self.preparingToDisplayViewList = false } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + return true } else { var canReply = true if component.slice.peer.id == component.context.account.peerId { @@ -1777,10 +1791,36 @@ public final class StoryItemSetContainerComponent: Component { if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id { self.animateNextNavigationId = nil self.viewListPanState = nil + self.isCompletingViewListPan = true itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) + itemsTransition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in + guard let self, completed else { + return + } + if self.isCompletingViewListPan { + self.isCompletingViewListPan = false + self.state?.updated(transition: .immediate) + } + }) resetScrollingOffsetWithItemTransition = true } + if let awaitingSwitchToId = self.awaitingSwitchToId, awaitingSwitchToId.to == component.slice.item.storyItem.id { + self.awaitingSwitchToId = nil + self.viewListPanState = nil + self.isCompletingViewListPan = true + itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) + itemsTransition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in + guard let self, completed else { + return + } + if self.isCompletingViewListPan { + self.isCompletingViewListPan = false + self.state?.updated(transition: .immediate) + } + }) + } + /*if self.topContentGradientLayer.colors == nil { var locations: [NSNumber] = [] var colors: [CGColor] = [] @@ -2114,15 +2154,29 @@ public final class StoryItemSetContainerComponent: Component { var maximizedBottomContentHeight: CGFloat = 0.0 var minimizedBottomContentFraction: CGFloat = 0.0 + let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0)) + let defaultHeight = 60.0 + component.safeInsets.bottom + 1.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] - if self.displayViewList { + if self.displayViewList, let viewListPanState = self.viewListPanState { if currentIndex != 0 { - visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id) + if viewListPanState.fraction > 0.0 { + visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id) + } } if currentIndex != component.slice.allItems.count - 1 { - visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id) + if viewListPanState.fraction < 0.0 { + visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id) + } + } + } + if self.viewListPanState != nil || self.isCompletingViewListPan { + for (id, _) in self.viewLists { + if !visibleViewListIds.contains(id) { + visibleViewListIds.append(id) + } } } @@ -2134,6 +2188,20 @@ public final class StoryItemSetContainerComponent: Component { var fixedAnimationOffset: CGFloat = 0.0 var applyFixedAnimationOffsetIds: [Int32] = [] + let outerExpansionFraction: CGFloat + let outerExpansionDirection: Bool + if self.displayViewList { + if let verticalPanState = self.verticalPanState { + outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction)) + } else { + outerExpansionFraction = 1.0 + } + outerExpansionDirection = false + } else { + outerExpansionFraction = component.verticalPanFraction + outerExpansionDirection = true + } + for id in visibleViewListIds { guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else { continue @@ -2154,23 +2222,6 @@ public final class StoryItemSetContainerComponent: Component { applyFixedAnimationOffsetIds.append(id) } - let outerExpansionFraction: CGFloat - let outerExpansionDirection: Bool - if self.displayViewList { - if let verticalPanState = self.verticalPanState { - outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction)) - } else { - outerExpansionFraction = 1.0 - } - outerExpansionDirection = false - } else if let _ = item.storyItem.views { - outerExpansionFraction = component.verticalPanFraction - outerExpansionDirection = true - } else { - outerExpansionFraction = 0.0 - outerExpansionDirection = true - } - viewList.view.parentState = state let viewListSize = viewList.view.update( transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint( @@ -2419,10 +2470,10 @@ public final class StoryItemSetContainerComponent: Component { } } if id == component.slice.item.storyItem.id { - viewListInset = viewList.externalState.minimizedHeight * viewList.externalState.minimizationFraction + viewList.externalState.defaultHeight * (1.0 - viewList.externalState.minimizationFraction) + viewListInset = minimizedHeight * viewList.externalState.minimizationFraction + defaultHeight * (1.0 - viewList.externalState.minimizationFraction) inputPanelBottomInset = viewListInset - minimizedBottomContentHeight = viewList.externalState.minimizedHeight - maximizedBottomContentHeight = viewList.externalState.defaultHeight + minimizedBottomContentHeight = minimizedHeight + maximizedBottomContentHeight = defaultHeight minimizedBottomContentFraction = viewList.externalState.minimizationFraction } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 29c13a8c23..339e80b4c9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -630,6 +630,26 @@ final class StoryItemSetViewListComponent: Component { var applyState = false var firstTime = true self.viewListDisposable = (viewList.state + |> mapToSignal { state in + #if DEBUG && false + if !state.items.isEmpty { + let otherItems: [EngineStoryViewListContext.Item] = Array(state.items.reversed().prefix(3)) + let otherState = EngineStoryViewListContext.State( + totalCount: 3, + items: otherItems, + loadMoreToken: state.loadMoreToken + ) + return .single(state) + |> then(.single(otherState) |> delay(1.0, queue: .mainQueue())) + |> then(.complete() |> delay(1.0, queue: .mainQueue())) + |> restart + } else { + return .single(state) + } + #else + return .single(state) + #endif + } |> deliverOnMainQueue).start(next: { [weak self] listState in guard let self else { return @@ -641,6 +661,7 @@ final class StoryItemSetViewListComponent: Component { self.ignoreScrolling = false } self.viewListState = listState + if applyState { self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: false))) } diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD index c37647c1e2..3a7676e609 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/TelegramCore", "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/SemanticStatusNode", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index b22810d66c..1683305c01 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -11,8 +11,17 @@ import MoreHeaderButton import SemanticStatusNode import SwiftSignalKit import TelegramPresentationData +import AnimatedCountLabelNode public final class StoryFooterPanelComponent: Component { + public final class AnimationHint { + public let synchronousLoad: Bool + + public init(synchronousLoad: Bool) { + self.synchronousLoad = synchronousLoad + } + } + public let context: AccountContext public let strings: PresentationStrings public let storyItem: EngineStoryItem? @@ -63,8 +72,8 @@ public final class StoryFooterPanelComponent: Component { public final class View: UIView { private let viewStatsButton: HighlightTrackingButton - private let viewStatsText = ComponentView() - private let viewStatsExpandedText = ComponentView() + private let viewStatsText: AnimatedCountLabelView + private let viewStatsExpandedText: AnimatedCountLabelView private let deleteButton = ComponentView() private var statusButton: HighlightableButton? @@ -72,7 +81,7 @@ public final class StoryFooterPanelComponent: Component { private var uploadingText: ComponentView? private let avatarsContext: AnimatedAvatarSetContext - private let avatarsNode: AnimatedAvatarSetNode + private let avatarsView: AnimatedAvatarSetView private var component: StoryFooterPanelComponent? private weak var state: EmptyComponentState? @@ -84,16 +93,18 @@ public final class StoryFooterPanelComponent: Component { override init(frame: CGRect) { self.viewStatsButton = HighlightTrackingButton() + self.viewStatsText = AnimatedCountLabelView(frame: CGRect()) + self.viewStatsExpandedText = AnimatedCountLabelView(frame: CGRect()) self.avatarsContext = AnimatedAvatarSetContext() - self.avatarsNode = AnimatedAvatarSetNode() + self.avatarsView = AnimatedAvatarSetView() self.externalContainerView = UIView() super.init(frame: frame) - self.avatarsNode.view.isUserInteractionEnabled = false - self.externalContainerView.addSubview(self.avatarsNode.view) + self.avatarsView.isUserInteractionEnabled = false + self.externalContainerView.addSubview(self.avatarsView) self.addSubview(self.viewStatsButton) self.viewStatsButton.highligthedChanged = { [weak self] highlighted in @@ -101,11 +112,11 @@ public final class StoryFooterPanelComponent: Component { return } if highlighted { - self.avatarsNode.view.alpha = 0.7 - self.viewStatsText.view?.alpha = 0.7 + self.avatarsView.alpha = 0.7 + self.viewStatsText.alpha = 0.7 } else { - self.avatarsNode.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) - self.viewStatsText.view?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.viewStatsText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) } } self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside) @@ -137,6 +148,13 @@ public final class StoryFooterPanelComponent: Component { } func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil + + var synchronousLoad = true + if let hint = transition.userData(AnimationHint.self) { + synchronousLoad = hint.synchronousLoad + } + if self.component?.storyItem?.id != component.storyItem?.id || self.component?.storyItem?.isPending != component.storyItem?.isPending { self.uploadProgressDisposable?.dispose() self.uploadProgress = 0.0 @@ -249,12 +267,12 @@ public final class StoryFooterPanelComponent: Component { peers = Array(seenPeers.prefix(3)) } let avatarsContent = self.avatarsContext.update(peers: peers, animated: false) - let avatarsSize = self.avatarsNode.update(context: component.context, content: avatarsContent, itemSize: CGSize(width: 30.0, height: 30.0), animated: false, synchronousLoad: true) + let avatarsSize = self.avatarsView.update(context: component.context, content: avatarsContent, itemSize: CGSize(width: 30.0, height: 30.0), animation: isFirstTime ? ListViewItemUpdateAnimation.None : ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false)), synchronousLoad: synchronousLoad) let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) - self.avatarsNode.position = avatarsNodeFrame.center - self.avatarsNode.bounds = CGRect(origin: CGPoint(), size: avatarsNodeFrame.size) - transition.setAlpha(view: self.avatarsNode.view, alpha: avatarsAlpha) + self.avatarsView.center = avatarsNodeFrame.center + self.avatarsView.bounds = CGRect(origin: CGPoint(), size: avatarsNodeFrame.size) + transition.setAlpha(view: self.avatarsView, alpha: avatarsAlpha) if !avatarsSize.width.isZero { leftOffset = avatarsNodeFrame.maxX + avatarSpacing } @@ -270,21 +288,28 @@ public final class StoryFooterPanelComponent: Component { } else { viewsText = component.strings.Story_Footer_Views(Int32(viewCount)) } + let _ = viewsText self.viewStatsButton.isEnabled = viewCount != 0 - let viewStatsTextSize = self.viewStatsText.update( - transition: .immediate, - component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: size.height) - ) - let viewStatsExpandedTextSize = self.viewStatsExpandedText.update( - transition: .immediate, - component: AnyComponent(Text(text: viewsText, font: Font.semibold(17.0), color: .white)), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: size.height) - ) + //TODO:localize + var regularSegments: [AnimatedCountLabelView.Segment] = [] + if viewCount != 0 { + regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white))) + } + regularSegments.append(.text(1, NSAttributedString(string: " Views", font: Font.regular(15.0), textColor: .white))) + + var expandedSegments: [AnimatedCountLabelView.Segment] = [] + if viewCount != 0 { + expandedSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.semibold(17.0), textColor: .white))) + } + expandedSegments.append(.text(1, NSAttributedString(string: " Views", font: Font.semibold(17.0), textColor: .white))) + + let viewStatsTextLayout = self.viewStatsText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) + let expandedViewStatsTextLayout = self.viewStatsExpandedText.update(size: CGSize(width: availableSize.width, height: size.height), segments: expandedSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) + + let viewStatsTextSize = viewStatsTextLayout.size + let viewStatsExpandedTextSize = expandedViewStatsTextLayout.size let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize) let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 3.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize) @@ -293,7 +318,9 @@ public final class StoryFooterPanelComponent: Component { let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction) let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter) - if let viewStatsTextView = self.viewStatsText.view { + do { + let viewStatsTextView = self.viewStatsText + if viewStatsTextView.superview == nil { viewStatsTextView.isUserInteractionEnabled = false self.externalContainerView.addSubview(viewStatsTextView) @@ -305,7 +332,9 @@ public final class StoryFooterPanelComponent: Component { } let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter) - if let viewStatsExpandedTextView = self.viewStatsExpandedText.view { + do { + let viewStatsExpandedTextView = self.viewStatsExpandedText + if viewStatsExpandedTextView.superview == nil { viewStatsExpandedTextView.isUserInteractionEnabled = false self.addSubview(viewStatsExpandedTextView) From 6c647ba70255abddd973cfb1e1c7bc320fd7ae89 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 1 Aug 2023 01:18:11 +0300 Subject: [PATCH 5/6] Display toast when unarchiving stories --- .../ChatListUI/Sources/ChatListController.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index a336107a6a..21c0036f32 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2900,7 +2900,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController undoValue = false } - if self.location != .chatList(groupId: .archive) { + if self.location == .chatList(groupId: .archive) { + self.present(UndoOverlayController(presentationData: self.presentationData, content: .archivedChat(peerId: peer.id.toInt64(), title: "", text: self.presentationData.strings.StoryFeed_TooltipUnarchive(peer.compactDisplayTitle).string, undo: true), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in + if case .undo = action { + if let self { + self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: undoValue) + } + } + return false + }), in: .current) + } else { self.present(UndoOverlayController(presentationData: self.presentationData, content: .archivedChat(peerId: peer.id.toInt64(), title: "", text: self.presentationData.strings.StoryFeed_TooltipArchive(peer.compactDisplayTitle).string, undo: true), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in if case .undo = action { if let self { From 069d5347dd5c918b50f21af7b6a3cb6ad8933056 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 1 Aug 2023 01:18:34 +0300 Subject: [PATCH 6/6] Support arbitrary resource download priorities --- .../Network/MultiplexedRequestManager.swift | 55 +++++++++++-------- .../Resources/TelegramEngineResources.swift | 4 +- .../Sources/StoryChatContent.swift | 18 ++++++ 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift index c8c3b54d50..7e1a846480 100644 --- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift +++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift @@ -92,7 +92,7 @@ struct NetworkResponseInfo { private final class MultiplexedRequestManagerContext { final class RequestManagerPriorityContext { - var resourceCounters: [String: Int] = [:] + var resourceCounters: [String: Bag] = [:] } private let queue: Queue @@ -123,19 +123,34 @@ private final class MultiplexedRequestManagerContext { } } - func pushPriority(resourceId: String) -> Disposable { + func pushPriority(resourceId: String, priority: Int) -> Disposable { let queue = self.queue - let value = self.priorityContext.resourceCounters[resourceId] ?? 0 - self.priorityContext.resourceCounters[resourceId] = value + 1 + let counters: Bag + if let current = self.priorityContext.resourceCounters[resourceId] { + counters = current + } else { + counters = Bag() + self.priorityContext.resourceCounters[resourceId] = counters + } - return ActionDisposable { [weak self] in + let index = counters.add(priority) + + self.updateState() + + return ActionDisposable { [weak self, weak counters] in queue.async { guard let `self` = self else { return } - let value = self.priorityContext.resourceCounters[resourceId] ?? 0 - self.priorityContext.resourceCounters[resourceId] = max(0, value - 1) + + if let current = self.priorityContext.resourceCounters[resourceId], current === counters { + current.remove(index) + if current.isEmpty { + self.priorityContext.resourceCounters.removeValue(forKey: resourceId) + } + self.updateState() + } } } } @@ -191,26 +206,22 @@ private final class MultiplexedRequestManagerContext { 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 + if let counters = self.priorityContext.resourceCounters[id] { + return counters.copyItems().max() ?? 0 } else { - return false + return 0 } - } ?? false + } ?? 0 let rhsPriority = rhs.resourceId.flatMap { id in - if let counters = self.priorityContext.resourceCounters[id], counters > 0 { - return true + if let counters = self.priorityContext.resourceCounters[id] { + return counters.copyItems().max() ?? 0 } else { - return false + return 0 } - } ?? false + } ?? 0 if lhsPriority != rhsPriority { - if lhsPriority { - return true - } else { - return false - } + return lhsPriority > rhsPriority } return lhs.id < rhs.id @@ -335,10 +346,10 @@ final class MultiplexedRequestManager { }) } - func pushPriority(resourceId: String) -> Disposable { + func pushPriority(resourceId: String, priority: Int) -> Disposable { let disposable = MetaDisposable() self.context.with { context in - disposable.set(context.pushPriority(resourceId: resourceId)) + disposable.set(context.pushPriority(resourceId: resourceId, priority: priority)) } return disposable } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index 122421d87b..e9f0aa9388 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -400,8 +400,8 @@ public extension TelegramEngine { preconditionFailure() } - public func pushPriorityDownload(resourceId: String) -> Disposable { - return self.account.network.multiplexedRequestManager.pushPriority(resourceId: resourceId) + public func pushPriorityDownload(resourceId: String, priority: Int = 1) -> Disposable { + return self.account.network.multiplexedRequestManager.pushPriority(resourceId: resourceId, priority: priority) } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 7ffbaecd9e..9e57d0a0a0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1460,6 +1460,23 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine var statusSignals: [Signal] = [] var loadSignals: [Signal] = [] + var fetchPriorityDisposable: Disposable? + + var fetchPriorityResourceId: String? + switch storyItem.media { + case let .image(image): + if let representation = largestImageRepresentation(image.representations) { + fetchPriorityResourceId = representation.resource.id.stringRepresentation + } + case let .file(file): + fetchPriorityResourceId = file.resource.id.stringRepresentation + default: + break + } + + if let fetchPriorityResourceId { + fetchPriorityDisposable = context.engine.resources.pushPriorityDownload(resourceId: fetchPriorityResourceId, priority: 2) + } switch storyItem.media { case let .image(image): @@ -1523,6 +1540,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine return ActionDisposable { statusDisposable.dispose() loadDisposable.dispose() + fetchPriorityDisposable?.dispose() } } }