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)