From 2ca5e9595a2d84c1e48f41c526655af611b78eab Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 12 Jun 2023 17:51:07 +0300 Subject: [PATCH 1/5] [Temp] Stories --- .../TelegramEngine/Messages/Stories.swift | 3 + .../StoryItemSetContainerComponent.swift | 33 +-- .../StoryItemSetViewListComponent.swift | 246 +++++++++++++++--- 3 files changed, 230 insertions(+), 52 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index ff352c23e5..18005b5035 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1312,6 +1312,9 @@ public final class EngineStoryViewListContext { } func loadMore() { + if !self.state.canLoadMore { + return + } if self.isLoadingMore { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index b1f0abbd3f..a38b0204c9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1006,10 +1006,7 @@ public final class StoryItemSetContainerComponent: Component { containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) ) - var currentItem: StoryContentItem? - currentItem = component.slice.item - - let footerPanelSize = self.footerPanel.update( + /*let footerPanelSize = self.footerPanel.update( transition: transition, component: AnyComponent(StoryFooterPanelComponent( context: component.context, @@ -1239,7 +1236,7 @@ public final class StoryItemSetContainerComponent: Component { )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) - ) + )*/ let bottomContentInsetWithoutInput = bottomContentInset var viewListInset: CGFloat = 0.0 @@ -1260,7 +1257,7 @@ public final class StoryItemSetContainerComponent: Component { inputPanelIsOverlay = true } - if self.displayViewList { + if component.slice.peer.id == component.context.account.peerId { let viewList: ViewList var viewListTransition = transition if let current = self.viewList { @@ -1273,6 +1270,7 @@ public final class StoryItemSetContainerComponent: Component { self.viewList = viewList } + viewList.view.parentState = state let viewListSize = viewList.view.update( transition: viewListTransition, component: AnyComponent(StoryItemSetViewListComponent( @@ -1282,6 +1280,7 @@ public final class StoryItemSetContainerComponent: Component { strings: component.strings, safeInsets: component.safeInsets, storyItem: component.slice.item.storyItem, + outerExpansionFraction: component.verticalPanFraction, close: { [weak self] in guard let self else { return @@ -1294,7 +1293,7 @@ public final class StoryItemSetContainerComponent: Component { containerSize: availableSize ) let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize) - if let viewListView = viewList.view.view { + if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { var animateIn = false if viewListView.superview == nil { self.addSubview(viewListView) @@ -1303,15 +1302,15 @@ public final class StoryItemSetContainerComponent: Component { viewListTransition.setFrame(view: viewListView, frame: viewListFrame) if animateIn, !transition.animation.isImmediate { - transition.animatePosition(view: viewListView, from: CGPoint(x: 0.0, y: viewListFrame.height), to: CGPoint(), additive: true) + viewListView.animateIn(transition: transition) } } - viewListInset = viewListFrame.height + viewListInset = viewList.externalState.effectiveHeight inputPanelBottomInset = viewListInset } else if let viewList = self.viewList { self.viewList = nil - if let viewListView = viewList.view.view { - transition.setPosition(view: viewListView, position: CGPoint(x: viewListView.center.x, y: availableSize.height + viewListView.bounds.height * 0.5), completion: { [weak viewListView] _ in + if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { + viewListView.animateOut(transition: transition, completion: { [weak viewListView] in viewListView?.removeFromSuperview() }) } @@ -1320,12 +1319,8 @@ public final class StoryItemSetContainerComponent: Component { let contentDefaultBottomInset: CGFloat = bottomContentInset let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset) - let contentVisualBottomInset: CGFloat - if self.displayViewList { - contentVisualBottomInset = viewListInset + 12.0 - } else { - contentVisualBottomInset = contentDefaultBottomInset - } + let contentVisualBottomInset: CGFloat = max(contentDefaultBottomInset, viewListInset) + let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset let contentVisualScale = contentVisualHeight / contentSize.height @@ -1721,7 +1716,7 @@ public final class StoryItemSetContainerComponent: Component { } } - var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) + /*var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) var footerPanelAlpha: CGFloat = (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0 if case .regular = component.metrics.widthClass { footerPanelAlpha *= component.visibilityFraction @@ -1735,7 +1730,7 @@ public final class StoryItemSetContainerComponent: Component { } transition.setFrame(view: footerPanelView, frame: footerPanelFrame) transition.setAlpha(view: footerPanelView, alpha: footerPanelAlpha) - } + }*/ let bottomGradientHeight = inputPanelSize.height + 32.0 transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index b6809d9566..5775368689 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -10,9 +10,13 @@ import AccountContext import SwiftSignalKit import TelegramStringFormatting import ShimmerEffect +import StoryFooterPanelComponent final class StoryItemSetViewListComponent: Component { final class ExternalState { + fileprivate(set) var minimizedHeight: CGFloat = 0.0 + fileprivate(set) var effectiveHeight: CGFloat = 0.0 + init() { } } @@ -23,6 +27,7 @@ final class StoryItemSetViewListComponent: Component { let strings: PresentationStrings let safeInsets: UIEdgeInsets let storyItem: EngineStoryItem + let outerExpansionFraction: CGFloat let close: () -> Void init( @@ -32,6 +37,7 @@ final class StoryItemSetViewListComponent: Component { strings: PresentationStrings, safeInsets: UIEdgeInsets, storyItem: EngineStoryItem, + outerExpansionFraction: CGFloat, close: @escaping () -> Void ) { self.externalState = externalState @@ -40,6 +46,7 @@ final class StoryItemSetViewListComponent: Component { self.strings = strings self.safeInsets = safeInsets self.storyItem = storyItem + self.outerExpansionFraction = outerExpansionFraction self.close = close } @@ -56,6 +63,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.storyItem != rhs.storyItem { return false } + if lhs.outerExpansionFraction != rhs.outerExpansionFraction { + return false + } return true } @@ -110,11 +120,30 @@ final class StoryItemSetViewListComponent: Component { return true } } + + private final class PanState { + var startContentOffsetY: CGFloat = 0.0 + var fraction: CGFloat = 0.0 + var accumulatedOffset: CGFloat = 0.0 + + init() { + + } + } + + private final class EventCycleState { + var ignoreScrolling: Bool = false + + init() { + } + } - final class View: UIView, UIScrollViewDelegate { + final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let navigationBarBackground: BlurredBackgroundView private let navigationSeparator: SimpleLayer - private let navigationTitle = ComponentView() + + private let navigationPanel = ComponentView() + private let navigationLeftButton = ComponentView() private let backgroundView: UIView @@ -138,6 +167,9 @@ final class StoryItemSetViewListComponent: Component { private var viewListState: EngineStoryViewListContext.State? private var requestedLoadMoreToken: EngineStoryViewListContext.LoadMoreToken? + private var dismissPanState: PanState? + private var eventCycleState: EventCycleState? + override init(frame: CGRect) { self.navigationBarBackground = BlurredBackgroundView(color: .clear, enableBlur: true) self.navigationSeparator = SimpleLayer() @@ -153,12 +185,18 @@ final class StoryItemSetViewListComponent: Component { self.scrollView.indicatorStyle = .white super.init(frame: frame) + + self.scrollView.delegate = self self.addSubview(self.backgroundView) self.addSubview(self.scrollView) self.addSubview(self.navigationBarBackground) self.layer.addSublayer(self.navigationSeparator) + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + self.addGestureRecognizer(panRecognizer) } required init?(coder: NSCoder) { @@ -169,16 +207,126 @@ final class StoryItemSetViewListComponent: Component { self.viewListDisposable?.dispose() } + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + if case .began = recognizer.state { + let dismissPanState = PanState() + dismissPanState.startContentOffsetY = 0.0 + self.dismissPanState = dismissPanState + } + + if let dismissPanState = self.dismissPanState { + let relativeTranslationY = recognizer.translation(in: self).y - dismissPanState.startContentOffsetY + let overflowY = self.scrollView.contentOffset.y - relativeTranslationY + + dismissPanState.accumulatedOffset += -overflowY + dismissPanState.accumulatedOffset = max(0.0, dismissPanState.accumulatedOffset) + + if dismissPanState.accumulatedOffset > 0.0 { + self.scrollView.contentOffset = CGPoint() + + let eventCycleState = EventCycleState() + eventCycleState.ignoreScrolling = true + self.eventCycleState = eventCycleState + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.eventCycleState = nil + } + } + + dismissPanState.startContentOffsetY = recognizer.translation(in: self).y + + self.state?.updated(transition: .immediate) + } + case .cancelled, .ended: + if let dismissPanState = self.dismissPanState { + self.dismissPanState = nil + + let relativeTranslationY = recognizer.translation(in: self).y - dismissPanState.startContentOffsetY + let overflowY = self.scrollView.contentOffset.y - relativeTranslationY + + dismissPanState.accumulatedOffset += -overflowY + dismissPanState.accumulatedOffset = max(0.0, dismissPanState.accumulatedOffset) + + if dismissPanState.accumulatedOffset > 0.0 { + self.scrollView.contentOffset = CGPoint() + + let eventCycleState = EventCycleState() + eventCycleState.ignoreScrolling = true + self.eventCycleState = eventCycleState + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.eventCycleState = nil + } + } + + let velocityY = recognizer.velocity(in: self).y + if dismissPanState.accumulatedOffset > 150.0 || (dismissPanState.accumulatedOffset > 0.0 && velocityY > 300.0) { + self.component?.close() + } else { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + } + default: + break + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.frame.contains(point) { + return nil + } + return super.hitTest(point, with: event) } + func animateIn(transition: Transition) { + let offset = self.bounds.height - self.navigationBarBackground.frame.minY + Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0)) + } + + func animateOut(transition: Transition, completion: @escaping () -> Void) { + let offset = self.bounds.height - self.navigationBarBackground.frame.minY + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in + completion() + }) + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { + if let eventCycleState = self.eventCycleState { + if eventCycleState.ignoreScrolling { + self.ignoreScrolling = true + scrollView.contentOffset = CGPoint() + self.ignoreScrolling = false + return + } + } + self.updateScrolling(transition: .immediate) } } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if let eventCycleState = self.eventCycleState { + if eventCycleState.ignoreScrolling { + targetContentOffset.pointee.y = 0.0 + } + } + } + private func updateScrolling(transition: Transition) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -315,7 +463,7 @@ final class StoryItemSetViewListComponent: Component { self.visiblePlaceholderViews.removeValue(forKey: id) } - if let viewList = self.viewList, let viewListState = self.viewListState, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 { + if let viewList = self.viewList, let viewListState = self.viewListState, viewListState.loadMoreToken != nil, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 { if self.requestedLoadMoreToken != viewListState.loadMoreToken { self.requestedLoadMoreToken = viewListState.loadMoreToken viewList.loadMore() @@ -330,7 +478,7 @@ final class StoryItemSetViewListComponent: Component { self.component = component self.state = state - let size = CGSize(width: availableSize.width, height: min(availableSize.height, 500.0)) + let minimizedHeight = min(availableSize.height, 488.0) if themeUpdated { self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor @@ -364,11 +512,11 @@ final class StoryItemSetViewListComponent: Component { let sideInset: CGFloat = 16.0 let navigationHeight: CGFloat = 56.0 - let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: navigationHeight)) + let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight), size: CGSize(width: availableSize.width, height: navigationHeight)) transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame) self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) let navigationLeftButtonSize = self.navigationLeftButton.update( transition: transition, @@ -384,7 +532,7 @@ final class StoryItemSetViewListComponent: Component { environment: {}, containerSize: CGSize(width: 120.0, height: 100.0) ) - let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: navigationLeftButtonSize) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY), size: navigationLeftButtonSize) if let navigationLeftButtonView = self.navigationLeftButton.view { if navigationLeftButtonView.superview == nil { self.addSubview(navigationLeftButtonView) @@ -392,36 +540,55 @@ final class StoryItemSetViewListComponent: Component { transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) } - let titleText: String - - let viewCount = self.viewListState?.totalCount ?? component.storyItem.views?.seenCount - if let viewCount { - if viewCount == 1 { - titleText = "1 View" - } else { - titleText = "\(viewCount) Views" - } - } else { - titleText = "No Views" - } - let navigationTitleSize = self.navigationTitle.update( - transition: .immediate, - component: AnyComponent(Text( - text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor + let navigationPanelSize = self.navigationPanel.update( + transition: transition, + component: AnyComponent(StoryFooterPanelComponent( + context: component.context, + storyItem: component.storyItem, + expandViewStats: { [weak self] in + guard let self else { + return + } + let _ = self + }, + deleteAction: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component + }, + moreAction: { [weak self] sourceView, gesture in + guard let self, let component = self.component else { + return + } + + let _ = component + } )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: navigationHeight) + containerSize: CGSize(width: availableSize.width, height: 200.0) ) - let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - navigationTitleSize.width) * 0.5), y: floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) + if let navigationPanelView = self.navigationPanel.view { + if navigationPanelView.superview == nil { + self.addSubview(navigationPanelView) + } + + let expandedNavigationPanelFrame = CGRect(origin: navigationBarFrame.origin, size: navigationPanelSize) + let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: availableSize.height - navigationPanelSize.height), size: navigationPanelSize) + + transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: component.outerExpansionFraction)) + } + + /*let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: navigationBarFrame.minY + floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) if let navigationTitleView = self.navigationTitle.view { if navigationTitleView.superview == nil { self.addSubview(navigationTitleView) } transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center) transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) - } + }*/ - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: size.height - navigationBarFrame.maxY))) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height))) let measureItemSize = self.measureItem.update( transition: .immediate, @@ -439,7 +606,7 @@ final class StoryItemSetViewListComponent: Component { } )), environment: {}, - containerSize: CGSize(width: size.width, height: 1000.0) + containerSize: CGSize(width: availableSize.width, height: 1000.0) ) if self.placeholderImage == nil || themeUpdated { @@ -467,9 +634,9 @@ final class StoryItemSetViewListComponent: Component { } let itemLayout = ItemLayout( - containerSize: size, + containerSize: CGSize(width: availableSize.width, height: minimizedHeight), bottomInset: component.safeInsets.bottom, - topInset: 0.0, + topInset: navigationHeight, sideInset: sideInset, itemHeight: measureItemSize.height, itemCount: self.viewListState?.items.count ?? 0 @@ -480,8 +647,8 @@ final class StoryItemSetViewListComponent: Component { self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - let scrollContentInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.minY), size: CGSize(width: availableSize.width, height: minimizedHeight))) + let scrollContentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) let scrollIndicatorInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: component.safeInsets.bottom, right: 0.0) if self.scrollView.contentInset != scrollContentInsets { self.scrollView.contentInset = scrollContentInsets @@ -495,8 +662,21 @@ final class StoryItemSetViewListComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) + + var dismissOffsetY: CGFloat = 0.0 + if let dismissPanState = self.dismissPanState { + dismissOffsetY = -dismissPanState.accumulatedOffset + } - return size + let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY + dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset + + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY)) + + component.externalState.minimizedHeight = minimizedHeight + component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, minimizedHeight + dismissOffsetY)) + + return availableSize } } From ab8d40b940143aaf4cf91ffc8ab4ae9f981b1d17 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 12 Jun 2023 21:11:51 +0300 Subject: [PATCH 2/5] [WIP] Stories --- .../StoryItemSetContainerComponent.swift | 263 +++++++++++++++++- .../StoryItemSetViewListComponent.swift | 67 +++-- .../Sources/StoryFooterPanelComponent.swift | 45 ++- 3 files changed, 341 insertions(+), 34 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a38b0204c9..1467a13e6b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -593,8 +593,16 @@ public final class StoryItemSetContainerComponent: Component { } func activateInput() { - if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { - inputPanelView.activateInput() + guard let component = self.component else { + return + } + if component.slice.peer.id == component.context.account.peerId { + self.displayViewList = true + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } else { + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { + inputPanelView.activateInput() + } } } @@ -621,6 +629,16 @@ public final class StoryItemSetContainerComponent: Component { ) footerPanelView.layer.animateAlpha(from: 0.0, to: footerPanelView.alpha, duration: 0.28) } + if let viewListView = self.viewList?.view.view { + viewListView.layer.animatePosition( + from: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY), + to: CGPoint(), + duration: 0.3, + timingFunction: kCAMediaTimingFunctionSpring, + additive: true + ) + viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28) + } if let captionItemView = self.captionItem?.view.view { captionItemView.layer.animatePosition( from: CGPoint(x: 0.0, y: self.bounds.height - captionItemView.frame.minY), @@ -704,6 +722,16 @@ public final class StoryItemSetContainerComponent: Component { ) footerPanelView.layer.animateAlpha(from: footerPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) } + if let viewListView = self.viewList?.view.view { + viewListView.layer.animatePosition( + from: CGPoint(), + to: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY), + duration: 0.3, + timingFunction: kCAMediaTimingFunctionSpring, + additive: true + ) + viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28) + } if let captionItemView = self.captionItem?.view.view { captionItemView.layer.animatePosition( from: CGPoint(), @@ -1270,6 +1298,13 @@ public final class StoryItemSetContainerComponent: Component { self.viewList = viewList } + let outerExpansionFraction: CGFloat + if self.displayViewList { + outerExpansionFraction = 1.0 + } else { + outerExpansionFraction = component.verticalPanFraction + } + viewList.view.parentState = state let viewListSize = viewList.view.update( transition: viewListTransition, @@ -1280,13 +1315,235 @@ public final class StoryItemSetContainerComponent: Component { strings: component.strings, safeInsets: component.safeInsets, storyItem: component.slice.item.storyItem, - outerExpansionFraction: component.verticalPanFraction, + outerExpansionFraction: outerExpansionFraction, close: { [weak self] in guard let self else { return } self.displayViewList = false self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }, + expandViewStats: { [weak self] in + guard let self else { + return + } + + if !self.displayViewList { + self.displayViewList = true + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + }, + deleteAction: { [weak self] in + guard let self, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component else { + return + } + component.delete() + + /*if let currentSlice = self.currentSlice, let index = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) { + let item = currentSlice.items[index] + + if currentSlice.items.count == 1 { + component.navigateToItemSet(.next) + } else { + var nextIndex: Int = index + 1 + if nextIndex >= currentSlice.items.count { + nextIndex = currentSlice.items.count - 1 + } + self.focusedItemId = currentSlice.items[nextIndex].id + + currentSlice.items[nextIndex].markAsSeen?() + + self.state?.updated(transition: .immediate) + } + + item.delete?() + }*/ + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + + actionSheet.dismissed = { [weak self] _ in + guard let self else { + return + } + self.actionSheet = nil + self.updateIsProgressPaused() + } + self.actionSheet = actionSheet + self.updateIsProgressPaused() + + component.presentController(actionSheet) + }, + moreAction: { [weak self] sourceView, gesture in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + + var items: [ContextMenuItem] = [] + + let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 + + let privacyText: String + switch component.slice.item.storyItem.privacy?.base { + case .closeFriends: + if additionalCount != 0 { + privacyText = "Close Friends (+\(additionalCount)" + } else { + privacyText = "Close Friends" + } + case .contacts: + if additionalCount != 0 { + privacyText = "Contacts (+\(additionalCount)" + } else { + privacyText = "Contacts" + } + case .nobody: + if additionalCount != 0 { + if additionalCount == 1 { + privacyText = "\(additionalCount) Person" + } else { + privacyText = "\(additionalCount) People" + } + } else { + privacyText = "Only Me" + } + default: + privacyText = "Everyone" + } + + items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openItemPrivacySettings() + }))) + + items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing() + }))) + + items.append(.separator) + + component.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() + + if component.slice.item.storyItem.isPinned { + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Story removed from your profile", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + )) + } else { + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + )) + } + }))) + items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + }, action: { _, a in + a(.default) + }))) + + if component.slice.item.storyItem.isPublic { + items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let component = self.component else { + return + } + if let link { + UIPasteboard.general.string = link + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: "Link copied."), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + )) + } + }) + }))) + items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { _, a in + a(.default) + }))) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.contextController = nil + self.updateIsProgressPaused() + } + self.contextController = contextController + self.updateIsProgressPaused() + controller.present(contextController, in: .window(.root)) } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 5775368689..0e953b1cfb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -29,6 +29,9 @@ final class StoryItemSetViewListComponent: Component { let storyItem: EngineStoryItem let outerExpansionFraction: CGFloat let close: () -> Void + let expandViewStats: () -> Void + let deleteAction: () -> Void + let moreAction: (UIView, ContextGesture?) -> Void init( externalState: ExternalState, @@ -38,7 +41,10 @@ final class StoryItemSetViewListComponent: Component { safeInsets: UIEdgeInsets, storyItem: EngineStoryItem, outerExpansionFraction: CGFloat, - close: @escaping () -> Void + close: @escaping () -> Void, + expandViewStats: @escaping () -> Void, + deleteAction: @escaping () -> Void, + moreAction: @escaping (UIView, ContextGesture?) -> Void ) { self.externalState = externalState self.context = context @@ -48,6 +54,9 @@ final class StoryItemSetViewListComponent: Component { self.storyItem = storyItem self.outerExpansionFraction = outerExpansionFraction self.close = close + self.expandViewStats = expandViewStats + self.deleteAction = deleteAction + self.moreAction = moreAction } static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool { @@ -284,6 +293,11 @@ final class StoryItemSetViewListComponent: Component { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let navigationPanelView = self.navigationPanel.view { + if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) { + return result + } + } if !self.backgroundView.frame.contains(point) { return nil } @@ -478,7 +492,7 @@ final class StoryItemSetViewListComponent: Component { self.component = component self.state = state - let minimizedHeight = min(availableSize.height, 488.0) + let minimizedHeight = min(availableSize.height, 500.0) if themeUpdated { self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor @@ -512,7 +526,7 @@ final class StoryItemSetViewListComponent: Component { let sideInset: CGFloat = 16.0 let navigationHeight: CGFloat = 56.0 - let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight), size: CGSize(width: availableSize.width, height: navigationHeight)) + let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight + 12.0), size: CGSize(width: availableSize.width, height: navigationHeight)) transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame) self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) @@ -540,29 +554,41 @@ final class StoryItemSetViewListComponent: Component { transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) } + let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY + + var dismissOffsetY: CGFloat = 0.0 + if let dismissPanState = self.dismissPanState { + dismissOffsetY = -dismissPanState.accumulatedOffset + } + + dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset + + let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset)) + let navigationPanelSize = self.navigationPanel.update( transition: transition, component: AnyComponent(StoryFooterPanelComponent( context: component.context, storyItem: component.storyItem, + expandFraction: dismissFraction, expandViewStats: { [weak self] in - guard let self else { + guard let self, let component = self.component else { return } - let _ = self + component.expandViewStats() }, deleteAction: { [weak self] in guard let self, let component = self.component else { return } - let _ = component + component.deleteAction() }, moreAction: { [weak self] sourceView, gesture in guard let self, let component = self.component else { return } - let _ = component + component.moreAction(sourceView, gesture) } )), environment: {}, @@ -573,21 +599,12 @@ final class StoryItemSetViewListComponent: Component { self.addSubview(navigationPanelView) } - let expandedNavigationPanelFrame = CGRect(origin: navigationBarFrame.origin, size: navigationPanelSize) - let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: availableSize.height - navigationPanelSize.height), size: navigationPanelSize) + let expandedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY + 4.0), size: navigationPanelSize) + let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY - navigationPanelSize.height - component.safeInsets.bottom - 1.0), size: navigationPanelSize) - transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: component.outerExpansionFraction)) + transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: dismissFraction)) } - /*let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: navigationBarFrame.minY + floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) - if let navigationTitleView = self.navigationTitle.view { - if navigationTitleView.superview == nil { - self.addSubview(navigationTitleView) - } - transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center) - transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) - }*/ - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height))) let measureItemSize = self.measureItem.update( @@ -662,19 +679,13 @@ final class StoryItemSetViewListComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) - - var dismissOffsetY: CGFloat = 0.0 - if let dismissPanState = self.dismissPanState { - dismissOffsetY = -dismissPanState.accumulatedOffset - } - - let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY - dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY)) component.externalState.minimizedHeight = minimizedHeight - component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, minimizedHeight + dismissOffsetY)) + + let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0) + component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight)) return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 7d9a7e1fa6..4754c71da0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -12,6 +12,7 @@ import MoreHeaderButton public final class StoryFooterPanelComponent: Component { public let context: AccountContext public let storyItem: EngineStoryItem? + public let expandFraction: CGFloat public let expandViewStats: () -> Void public let deleteAction: () -> Void public let moreAction: (UIView, ContextGesture?) -> Void @@ -19,6 +20,7 @@ public final class StoryFooterPanelComponent: Component { public init( context: AccountContext, storyItem: EngineStoryItem?, + expandFraction: CGFloat, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void @@ -26,6 +28,7 @@ public final class StoryFooterPanelComponent: Component { self.context = context self.storyItem = storyItem self.expandViewStats = expandViewStats + self.expandFraction = expandFraction self.deleteAction = deleteAction self.moreAction = moreAction } @@ -37,12 +40,16 @@ public final class StoryFooterPanelComponent: Component { if lhs.storyItem != rhs.storyItem { return false } + if lhs.expandFraction != rhs.expandFraction { + return false + } return true } public final class View: UIView { private let viewStatsButton: HighlightableButton private let viewStatsText = ComponentView() + private let viewStatsExpandedText = ComponentView() private let deleteButton = ComponentView() private var moreButton: MoreHeaderButton? @@ -98,6 +105,8 @@ public final class StoryFooterPanelComponent: Component { let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) self.avatarsNode.frame = avatarsNodeFrame + //transition.setScale(view: self.avatarsNode.view, scale: CGFloat(1.0).interpolate(to: 0.001, amount: component.expandFraction)) + transition.setAlpha(view: self.avatarsNode.view, alpha: pow(1.0 - component.expandFraction, 1.0)) if !avatarsSize.width.isZero { leftOffset = avatarsNodeFrame.maxX + avatarSpacing } @@ -124,18 +133,45 @@ public final class StoryFooterPanelComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: size.height) ) - let viewStatsTextFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize) + 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) + ) + + 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: floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize) + let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction) + + let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction) + + let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter) if let viewStatsTextView = self.viewStatsText.view { if viewStatsTextView.superview == nil { - viewStatsTextView.layer.anchorPoint = CGPoint() viewStatsTextView.isUserInteractionEnabled = false self.viewStatsButton.addSubview(viewStatsTextView) } - transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin) + transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.center) transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size)) + transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2)) + transition.setScale(view: viewStatsTextView, scale: viewStatsCurrentFrame.width / viewStatsTextFrame.width) + } + + let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter) + if let viewStatsExpandedTextView = self.viewStatsExpandedText.view { + if viewStatsExpandedTextView.superview == nil { + viewStatsExpandedTextView.isUserInteractionEnabled = false + self.viewStatsButton.addSubview(viewStatsExpandedTextView) + } + transition.setPosition(view: viewStatsExpandedTextView, position: viewStatsExpandedTextFrame.center) + transition.setBounds(view: viewStatsExpandedTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsExpandedTextFrame.size)) + transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2)) + transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width) } transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0))) + self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0 var rightContentOffset: CGFloat = availableSize.width - 12.0 @@ -162,6 +198,8 @@ public final class StoryFooterPanelComponent: Component { } transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize)) rightContentOffset -= deleteButtonSize.width + 8.0 + + transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0)) } let moreButton: MoreHeaderButton @@ -197,6 +235,7 @@ public final class StoryFooterPanelComponent: Component { let buttonSize = CGSize(width: 32.0, height: 44.0) moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)) + transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0)) return size } From abd345ec0f69107b391c6bae4addeaec284972f2 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 12 Jun 2023 22:26:54 +0300 Subject: [PATCH 3/5] [WIP] Stories --- .../Sources/ChatListControllerNode.swift | 9 ++- .../Sources/Node/ChatListNode.swift | 15 ++-- .../Sources/ContactsControllerNode.swift | 2 +- .../Sources/ChatListNavigationBar.swift | 81 +++++++++++-------- .../StoryItemSetContainerComponent.swift | 3 +- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 7974b65990..da8ffefc24 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1957,8 +1957,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { offset = 0.0 } + var allowAvatarsExpansion: Bool = true + if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.mainContainerNode.storiesUnlocked { + allowAvatarsExpansion = false + } + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, transition: Transition(transition)) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, transition: Transition(transition)) } } @@ -2209,7 +2214,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { func willScrollToTop() { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: 0.0, transition: Transition(animation: .curve(duration: 0.3, curve: .slide))) + navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: Transition(animation: .curve(duration: 0.3, curve: .slide))) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 54e6ef2d50..fe5bf95e5b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1193,6 +1193,8 @@ public final class ChatListNode: ListView { } } + public private(set) var startedScrollingAtUpperBound: Bool = false + public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) { self.context = context self.location = location @@ -2719,7 +2721,6 @@ public final class ChatListNode: ListView { } } } - var startedScrollingAtUpperBound = false var startedScrollingWithCanExpandHiddenItems = false self.beganInteractiveDragging = { [weak self] _ in @@ -2727,10 +2728,10 @@ public final class ChatListNode: ListView { return } switch strongSelf.visibleContentOffset() { - case .none, .unknown: - startedScrollingAtUpperBound = false - case let .known(value): - startedScrollingAtUpperBound = value <= 0.0 + case .none, .unknown: + strongSelf.startedScrollingAtUpperBound = false + case let .known(value): + strongSelf.startedScrollingAtUpperBound = value <= 0.0 } if let canExpandHiddenItems = strongSelf.canExpandHiddenItems { @@ -2752,7 +2753,7 @@ public final class ChatListNode: ListView { guard let strongSelf = self else { return } - startedScrollingAtUpperBound = false + strongSelf.startedScrollingAtUpperBound = false let _ = strongSelf.contentScrollingEnded?(strongSelf) let revealHiddenItems: Bool switch strongSelf.visibleContentOffset() { @@ -2795,7 +2796,7 @@ public final class ChatListNode: ListView { atTop = false case let .known(value): atTop = value <= 0.0 - if startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking { + if strongSelf.startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking { revealHiddenItems = value <= -60.0 } } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 145357d2de..11eff8d81b 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -435,7 +435,7 @@ final class ContactsControllerNode: ASDisplayNode { } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, transition: Transition(transition)) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: true, transition: Transition(transition)) } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 235181bea9..a8111a250c 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -142,6 +142,7 @@ public final class ChatListNavigationBar: Component { private var currentLayout: CurrentLayout? private var rawScrollOffset: CGFloat? + private var currentAllowAvatarsExpansion: Bool = false public private(set) var clippedScrollOffset: CGFloat? public var deferScrollApplication: Bool = false @@ -191,14 +192,19 @@ public final class ChatListNavigationBar: Component { public func applyCurrentScroll(transition: Transition) { if let rawScrollOffset = self.rawScrollOffset, self.hasDeferredScrollOffset { - self.applyScroll(offset: rawScrollOffset, transition: transition) + self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: transition) } } - public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) { + public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) { + if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion { + self.addStoriesUnlockedAnimation(duration: 0.3) + } + let transition = transition self.rawScrollOffset = offset + self.currentAllowAvatarsExpansion = allowAvatarsExpansion if self.deferScrollApplication && !forceUpdate { self.hasDeferredScrollOffset = true @@ -286,7 +292,7 @@ public final class ChatListNavigationBar: Component { let clippedStoriesOffset = max(0.0, min(clippedScrollOffset, defaultStoriesOffsetDistance)) var storiesOffsetFraction: CGFloat var storiesUnlockedOffsetFraction: CGFloat - if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { + if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, allowAvatarsExpansion { if component.storiesUnlocked { storiesOffsetFraction = clippedStoriesOffset / defaultStoriesOffsetDistance storiesUnlockedOffsetFraction = 1.0 @@ -490,45 +496,52 @@ public final class ChatListNavigationBar: Component { if uploadProgressUpdated { if let rawScrollOffset = self.rawScrollOffset { - self.applyScroll(offset: rawScrollOffset, forceUpdate: true, transition: transition) + self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, forceUpdate: true, transition: transition) } } if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation { - self.applyScrollFractionAnimator?.invalidate() - self.applyScrollFractionAnimator = nil - - self.storiesOffsetStartFraction = self.storiesOffsetFraction - self.storiesUnlockedStartFraction = self.storiesUnlockedFraction - - let storiesUnlocked = component.storiesUnlocked - - self.applyScrollFraction = 0.0 - self.applyScrollUnlockedFraction = 0.0 - self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in - guard let self else { - return - } - - let t = listViewAnimationCurveSystem(value) - self.applyScrollFraction = t - self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t) - - if let rawScrollOffset = self.rawScrollOffset { - self.hasDeferredScrollOffset = true - self.applyScroll(offset: rawScrollOffset, transition: transition) - } - }, completion: { [weak self] in - guard let self else { - return - } - self.applyScrollFractionAnimator?.invalidate() - self.applyScrollFractionAnimator = nil - }) + self.addStoriesUnlockedAnimation(duration: duration) } return size } + + private func addStoriesUnlockedAnimation(duration: Double) { + guard let component = self.component else { + return + } + self.applyScrollFractionAnimator?.invalidate() + self.applyScrollFractionAnimator = nil + + self.storiesOffsetStartFraction = self.storiesOffsetFraction + self.storiesUnlockedStartFraction = self.storiesUnlockedFraction + + let storiesUnlocked = component.storiesUnlocked + + self.applyScrollFraction = 0.0 + self.applyScrollUnlockedFraction = 0.0 + self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in + guard let self else { + return + } + + let t = listViewAnimationCurveSystem(value) + self.applyScrollFraction = t + self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t) + + if let rawScrollOffset = self.rawScrollOffset { + self.hasDeferredScrollOffset = true + self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: .immediate) + } + }, completion: { [weak self] in + guard let self else { + return + } + self.applyScrollFractionAnimator?.invalidate() + self.applyScrollFractionAnimator = nil + }) + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 1467a13e6b..d1f22cf607 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -728,9 +728,10 @@ public final class StoryItemSetContainerComponent: Component { to: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, + removeOnCompletion: false, additive: true ) - viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28) + viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false) } if let captionItemView = self.captionItem?.view.view { captionItemView.layer.animatePosition( From 7150af3fe1e5e5654c5c4024781e08e430678c75 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 13 Jun 2023 09:44:42 +0300 Subject: [PATCH 4/5] Fix flag --- .../TelegramCore/Sources/TelegramEngine/Messages/Stories.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 18005b5035..35870d3ab9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -662,6 +662,8 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: } } + flags |= 1 << 3 + return account.network.request(Api.functions.stories.sendStory( flags: flags, media: inputMedia, From ceda80c3a5129b31c00db5a6595333f79d1dbdbc Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 13 Jun 2023 14:18:16 +0300 Subject: [PATCH 5/5] [WIP] Stories --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../Sources/ChatListController.swift | 117 ++--- .../Sources/ChatListSearchListPaneNode.swift | 2 +- .../Sources/InstantPageLayout.swift | 2 +- submodules/PeerInfoAvatarListNode/BUILD | 3 + .../Sources/PeerInfoAvatarListNode.swift | 53 +++ submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api23.swift | 76 ++-- submodules/TelegramApi/Sources/Api24.swift | 40 ++ .../ApiUtils/TelegramMediaWebpage.swift | 23 +- .../Sources/State/AccountViewTracker.swift | 2 + .../SyncCore_TelegramMediaWebpage.swift | 31 +- .../ChangePeerNotificationSettings.swift | 30 ++ .../Peers/TelegramEnginePeers.swift | 5 + .../TelegramCore/Sources/WebpagePreview.swift | 2 +- submodules/TelegramUI/BUILD | 1 + .../Sources/ChatListNavigationBar.swift | 10 +- .../AvatarStoryIndicatorComponent.swift | 14 +- .../StoryItemSetContainerComponent.swift | 2 +- .../Sources/StoryItemContentComponent.swift | 4 +- .../Sources/StoryPeerListItemComponent.swift | 8 +- .../Stories/StorySetIndicatorComponent/BUILD | 26 ++ .../Sources/StorySetIndicatorComponent.swift | 415 ++++++++++++++++++ .../Sources/ChatHistoryListNode.swift | 4 + .../ChatMessageAttachedContentNode.swift | 16 + .../ChatMessageWebpageBubbleContentNode.swift | 10 +- .../TelegramUI/Sources/OpenChatMessage.swift | 49 ++- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 19 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 138 +++--- 29 files changed, 913 insertions(+), 195 deletions(-) create mode 100644 submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5776ae5c81..992d1d0279 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -263,6 +263,11 @@ "PUSH_CHAT_REQ_JOINED" = "%2$@|%1$@ was accepted into the group"; +"PUSH_STORY_NOTEXT" = "%1$@|posted a story"; +"PUSH_MESSAGE_STORY" = "%1$@|shared a story with you"; +"PUSH_CHANNEL_MESSAGE_STORY" = "%1$@|shared a story"; +"PUSH_CHAT_MESSAGE_STORY" = "%2$@|%1$@ shared a story to the group"; + "LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages"; "LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages"; "LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ec5f16b7f4..dfecee9570 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2387,63 +2387,80 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - var items: [ContextMenuItem] = [] - - //TODO:localize - if peer.id == self.context.account.peerId { - items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return - } - - self.openStoryCamera() - }) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { [weak self] notificationSettings in + guard let self else { + return + } + + var items: [ContextMenuItem] = [] + + //TODO:localize + if peer.id == self.context.account.peerId { + items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c.dismiss(completion: { guard let self else { return } - guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + + self.openStoryCamera() + }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self else { return } - (self.navigationController as? NavigationController)?.pushViewController(controller) + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (self.navigationController as? NavigationController)?.pushViewController(controller) + }) }) - }) - }))) - /*items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - })))*/ - items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) + }))) - guard let self else { - return - } - self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true) - }))) - } - - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) - self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) + let isMuted = notificationSettings.storiesMuted == true + items.append(.action(ContextMenuActionItem(text: isMuted ? "Unmute" : "Mute", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Muted": "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + let _ = self.context.engine.peers.togglePeerStoriesMuted(peerId: peer.id).start() + }))) + + items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self else { + return + } + self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true) + }))) + } + + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) + }) } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 510ffaaffe..d56cc396f5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3464,7 +3464,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { return nil case .links: var media: [EngineMedia] = [] - media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, attributes: [], instantPage: nil))))) + media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))))) let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index 686cebb5a5..d6323cd19c 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) let item: InstantPageItem if let url = url, let coverId = coverId, case let .image(image) = media[coverId] { - let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil) + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil) let content = TelegramMediaWebpageContent.Loaded(loadedContent) item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) diff --git a/submodules/PeerInfoAvatarListNode/BUILD b/submodules/PeerInfoAvatarListNode/BUILD index f07f8b1e65..7ce9aba95e 100644 --- a/submodules/PeerInfoAvatarListNode/BUILD +++ b/submodules/PeerInfoAvatarListNode/BUILD @@ -26,6 +26,9 @@ swift_library( "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/AccountContext:AccountContext", "//submodules/AvatarVideoNode:AvatarVideoNode", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index d6d11bf2e9..cb928e8f13 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -16,6 +16,9 @@ import RadialStatusNode import TelegramUIPreferences import AvatarNode import AvatarVideoNode +import ComponentFlow +import ComponentDisplayAdapters +import StorySetIndicatorComponent private class PeerInfoAvatarListLoadingStripNode: ASImageNode { private var currentInHierarchy = false @@ -577,6 +580,9 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { public let topShadowNode: ASImageNode public let bottomShadowNode: ASImageNode + public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool)? + private var expandedStorySetIndicator: ComponentView? + public let contentNode: ASDisplayNode let leftHighlightNode: ASDisplayNode let rightHighlightNode: ASDisplayNode @@ -612,6 +618,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? public var currentIndexUpdated: (() -> Void)? + public var openStories: (() -> Void)? + public let isReady = Promise() private var didSetReady = false @@ -914,6 +922,12 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.isExpanded, let expandedStorySetIndicatorView = self.expandedStorySetIndicator?.view { + if let result = expandedStorySetIndicatorView.hitTest(self.view.convert(point, to: expandedStorySetIndicatorView), with: event) { + return result + } + } + return super.hitTest(point, with: event) } @@ -1228,6 +1242,45 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { })) } self.updateItems(size: size, transition: transition, stripTransition: transition) + + if let storyParams = self.storyParams { + var indicatorTransition = Transition(transition) + let expandedStorySetIndicator: ComponentView + if let current = self.expandedStorySetIndicator { + expandedStorySetIndicator = current + } else { + indicatorTransition = .immediate + expandedStorySetIndicator = ComponentView() + self.expandedStorySetIndicator = expandedStorySetIndicator + } + + let expandedStorySetSize = expandedStorySetIndicator.update( + transition: indicatorTransition, + component: AnyComponent(StorySetIndicatorComponent( + context: self.context, + peer: storyParams.peer, + items: storyParams.items, + hasUnseen: storyParams.hasUnseen, + totalCount: storyParams.count, + theme: defaultDarkPresentationTheme, + action: { [weak self] in + self?.openStories?() + } + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + let expandedStorySetFrame = CGRect(origin: CGPoint(x: floor((size.width - expandedStorySetSize.width) * 0.5), y: 10.0), size: expandedStorySetSize) + if let expandedStorySetIndicatorView = expandedStorySetIndicator.view { + if expandedStorySetIndicatorView.superview == nil { + self.stripContainerNode.view.addSubview(expandedStorySetIndicatorView) + } + indicatorTransition.setFrame(view: expandedStorySetIndicatorView, frame: expandedStorySetFrame) + } + } else if let expandedStorySetIndicator = self.expandedStorySetIndicator { + self.expandedStorySetIndicator = nil + expandedStorySetIndicator.view?.removeFromSuperview() + } } private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index a89a061af9..ea846884af 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -961,6 +961,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) } dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) } dict[-981018084] = { return Api.WebPage.parse_webPagePending($0) } + dict[-1818605967] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) } dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) } diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index f255fdec59..4ff5c87558 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -187,11 +187,21 @@ public extension Api { } } public extension Api { - enum WebPageAttribute: TypeConstructorDescription { + indirect enum WebPageAttribute: TypeConstructorDescription { + case webPageAttributeStory(flags: Int32, userId: Int64, id: Int32, story: Api.StoryItem?) case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .webPageAttributeStory(let flags, let userId, let id, let story): + if boxed { + buffer.appendInt32(-1818605967) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)} + break case .webPageAttributeTheme(let flags, let documents, let settings): if boxed { buffer.appendInt32(1421174295) @@ -209,11 +219,35 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .webPageAttributeStory(let flags, let userId, let id, let story): + return ("webPageAttributeStory", [("flags", flags as Any), ("userId", userId as Any), ("id", id as Any), ("story", story as Any)]) case .webPageAttributeTheme(let flags, let documents, let settings): return ("webPageAttributeTheme", [("flags", flags as Any), ("documents", documents as Any), ("settings", settings as Any)]) } } + public static func parse_webPageAttributeStory(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.StoryItem? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StoryItem + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.WebPageAttribute.webPageAttributeStory(flags: _1!, userId: _2!, id: _3!, story: _4) + } + else { + return nil + } + } public static func parse_webPageAttributeTheme(_ reader: BufferReader) -> WebPageAttribute? { var _1: Int32? _1 = reader.readInt32() @@ -1292,43 +1326,3 @@ public extension Api.account { } } -public extension Api.account { - enum TmpPassword: TypeConstructorDescription { - case tmpPassword(tmpPassword: Buffer, validUntil: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .tmpPassword(let tmpPassword, let validUntil): - if boxed { - buffer.appendInt32(-614138572) - } - serializeBytes(tmpPassword, buffer: buffer, boxed: false) - serializeInt32(validUntil, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .tmpPassword(let tmpPassword, let validUntil): - return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)]) - } - } - - public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index b154cef686..babd3e977a 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -1,3 +1,43 @@ +public extension Api.account { + enum TmpPassword: TypeConstructorDescription { + case tmpPassword(tmpPassword: Buffer, validUntil: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .tmpPassword(let tmpPassword, let validUntil): + if boxed { + buffer.appendInt32(-614138572) + } + serializeBytes(tmpPassword, buffer: buffer, boxed: false) + serializeInt32(validUntil, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .tmpPassword(let tmpPassword, let validUntil): + return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)]) + } + } + + public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!) + } + else { + return nil + } + } + + } +} public extension Api.account { enum WallPapers: TypeConstructorDescription { case wallPapers(hash: Int64, wallpapers: [Api.WallPaper]) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index a727eb4fd4..8686b470a9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -6,12 +6,14 @@ import TelegramApi func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? { switch attribute { - case let .webPageAttributeTheme(_, documents, settings): - var files: [TelegramMediaFile] = [] - if let documents = documents { - files = documents.compactMap { telegramMediaFileFromApiDocument($0) } - } - return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) + case let .webPageAttributeTheme(_, documents, settings): + var files: [TelegramMediaFile] = [] + if let documents = documents { + files = documents.compactMap { telegramMediaFileFromApiDocument($0) } + } + return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) + case .webPageAttributeStory: + return nil } } @@ -38,15 +40,22 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> if let document = document { file = telegramMediaFileFromApiDocument(document) } + var story: TelegramMediaStory? var webpageAttributes: [TelegramMediaWebpageAttribute] = [] if let attributes = attributes { webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute) + for attribute in attributes { + if case let .webPageAttributeStory(_, userId, id, _) = attribute { + story = TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id)) + } + } } + var instantPage: InstantPage? if let cachedPage = cachedPage { instantPage = InstantPage(apiPage: cachedPage) } - return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, attributes: webpageAttributes, instantPage: instantPage))) + return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage))) case .webPageEmpty: return nil } diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index baf12fa36c..f0afb02650 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1254,6 +1254,8 @@ public final class AccountViewTracker { for media in message.media { if let storyMedia = media as? TelegramMediaStory { result.insert(storyMedia.storyId) + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let story = content.story { + result.insert(story.storyId) } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index 538cbefc82..8387c277fc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -85,10 +85,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let image: TelegramMediaImage? public let file: TelegramMediaFile? + public let story: TelegramMediaStory? public let attributes: [TelegramMediaWebpageAttribute] public let instantPage: InstantPage? - public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) { + public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, story: TelegramMediaStory?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) { self.url = url self.displayUrl = displayUrl self.hash = hash @@ -103,6 +104,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.author = author self.image = image self.file = file + self.story = story self.attributes = attributes self.instantPage = instantPage } @@ -141,6 +143,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.file = nil } + if let story = decoder.decodeObjectForKey("stry") as? TelegramMediaStory { + self.story = story + } else { + self.story = nil + } + var effectiveAttributes: [TelegramMediaWebpageAttribute] = [] if let attributes = decoder.decodeObjectArrayWithDecoderForKey("attr") as [TelegramMediaWebpageAttribute]? { effectiveAttributes.append(contentsOf: attributes) @@ -218,6 +226,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "fi") } + if let story = self.story { + encoder.encodeObject(story, forKey: "stry") + } else { + encoder.encodeNil(forKey: "stry") + } encoder.encodeObjectArray(self.attributes, forKey: "attr") @@ -261,6 +274,14 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage return false } + if let lhsStory = lhs.story, let rhsStory = rhs.story { + if !lhsStory.isEqual(to: rhsStory) { + return false + } + } else if (lhs.story == nil) != (rhs.story == nil) { + return false + } + if lhs.attributes.count != rhs.attributes.count { return false } else { @@ -289,6 +310,14 @@ public final class TelegramMediaWebpage: Media, Equatable { } public let peerIds: [PeerId] = [] + public var storyIds: [StoryId] { + if case let .Loaded(content) = self.content, let story = content.story { + return story.storyIds + } else { + return [] + } + } + public let webpageId: MediaId public let content: TelegramMediaWebpageContent diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift index c170533080..d565c8f018 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift @@ -62,6 +62,36 @@ func _internal_togglePeerMuted(account: Account, peerId: PeerId, threadId: Int64 } } +func _internal_togglePeerStoriesMuted(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Void in + guard let peer = transaction.getPeer(peerId) else { + return + } + + var notificationPeerId = peerId + if let associatedPeerId = peer.associatedPeerId { + notificationPeerId = associatedPeerId + } + + let currentSettings = transaction.getPeerNotificationSettings(id: notificationPeerId) as? TelegramPeerNotificationSettings + let previousSettings: TelegramPeerNotificationSettings + if let currentSettings = currentSettings { + previousSettings = currentSettings + } else { + previousSettings = TelegramPeerNotificationSettings.defaultSettings + } + + let updatedSettings: TelegramPeerNotificationSettings + if let previousStoriesMuted = previousSettings.storiesMuted { + updatedSettings = previousSettings.withUpdatedStoriesMuted(!previousStoriesMuted) + } else { + updatedSettings = previousSettings.withUpdatedStoriesMuted(true) + } + + transaction.updatePendingPeerNotificationSettings(peerId: notificationPeerId, settings: updatedSettings) + } +} + func _internal_updatePeerMuteSetting(account: Account, peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal { return account.postbox.transaction { transaction -> Void in _internal_updatePeerMuteSetting(account: account, transaction: transaction, peerId: peerId, threadId: threadId, muteInterval: muteInterval) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index fde91bc8c3..c2f0687e19 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -246,6 +246,11 @@ public extension TelegramEngine { public func togglePeerMuted(peerId: PeerId, threadId: Int64?) -> Signal { return _internal_togglePeerMuted(account: self.account, peerId: peerId, threadId: threadId) } + + public func togglePeerStoriesMuted(peerId: EnginePeer.Id) -> Signal { + return _internal_togglePeerStoriesMuted(account: self.account, peerId: peerId) + |> ignoreValues + } public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal { return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval) diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index c7bd6363dd..132b1366a2 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -88,7 +88,7 @@ public func actualizedWebpage(postbox: Postbox, network: Network, webpage: Teleg return updatedWebpage } } else if let result = result, case let .webPageNotModified(_, viewsValue) = result, let views = viewsValue, case let .Loaded(content) = webpage.content { - let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }))) + let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }))) let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent) return postbox.transaction { transaction -> TelegramMediaWebpage in updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 81f1becf9e..1adb3e90b4 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -374,6 +374,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index a8111a250c..0fcb80d423 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -198,7 +198,7 @@ public final class ChatListNavigationBar: Component { public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) { if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion { - self.addStoriesUnlockedAnimation(duration: 0.3) + self.addStoriesUnlockedAnimation(duration: 0.3, animateScrollUnlocked: false) } let transition = transition @@ -501,13 +501,13 @@ public final class ChatListNavigationBar: Component { } if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation { - self.addStoriesUnlockedAnimation(duration: duration) + self.addStoriesUnlockedAnimation(duration: duration, animateScrollUnlocked: true) } return size } - private func addStoriesUnlockedAnimation(duration: Double) { + private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) { guard let component = self.component else { return } @@ -528,7 +528,9 @@ public final class ChatListNavigationBar: Component { let t = listViewAnimationCurveSystem(value) self.applyScrollFraction = t - self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t) + if animateScrollUnlocked { + self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t) + } if let rawScrollOffset = self.rawScrollOffset { self.hasDeferredScrollOffset = true diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index 2c21713e0a..0dd1a6dd55 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -6,17 +6,23 @@ import TelegramPresentationData public final class AvatarStoryIndicatorComponent: Component { public let hasUnseen: Bool + public let isDarkTheme: Bool public init( - hasUnseen: Bool + hasUnseen: Bool, + isDarkTheme: Bool ) { self.hasUnseen = hasUnseen + self.isDarkTheme = isDarkTheme } public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { if lhs.hasUnseen != rhs.hasUnseen { return false } + if lhs.isDarkTheme != rhs.isDarkTheme { + return false + } return true } @@ -69,7 +75,11 @@ public final class AvatarStoryIndicatorComponent: Component { if component.hasUnseen { colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] } else { - colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + if component.isDarkTheme { + colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } } let colorSpace = CGColorSpaceCreateDeviceRGB() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index d1f22cf607..d1ed2e215b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1497,7 +1497,7 @@ public final class StoryItemSetContainerComponent: Component { a(.default) }))) - if component.slice.item.storyItem.isPublic { + if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index 8718b33d37..280d124063 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -340,9 +340,9 @@ final class StoryItemContentComponent: Component { var messageMedia: EngineMedia? switch component.item.media { case let .image(image): - messageMedia = .image(image) + messageMedia = .image(image) case let .file(file): - messageMedia = .file(file) + messageMedia = .file(file) default: break } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 8e49f2e203..2b13335b21 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -635,14 +635,18 @@ public final class StoryPeerListItemComponent: Component { self.indicatorShapeLayer.lineWidth = indicatorLineWidth - if hadUnseen != component.hasUnseen || hadProgress != (component.ringAnimation != nil) { + if hadUnseen != component.hasUnseen || themeUpdated || hadProgress != (component.ringAnimation != nil) { let locations: [CGFloat] = [0.0, 1.0] let colors: [CGColor] if component.hasUnseen || component.ringAnimation != nil { colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] } else { - colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + if component.theme.overallDarkAppearance { + colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } } self.indicatorColorLayer.locations = locations.map { $0 as NSNumber } diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD new file mode 100644 index 0000000000..49b18603a1 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StorySetIndicatorComponent", + module_name = "StorySetIndicatorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/PhotoResources", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift new file mode 100644 index 0000000000..24de203d8c --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -0,0 +1,415 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramCore +import Postbox +import SwiftSignalKit +import AccountContext +import PhotoResources + +private final class ShapeImageView: UIView { + struct Item: Equatable { + var position: CGPoint + var diameter: CGFloat + var image: UIImage? + } + + struct Params: Equatable { + var items: [Item] + var innerSpacing: CGFloat + var lineWidth: CGFloat + var borderColors: [UInt32] + } + + var params: Params? + + override func draw(_ rect: CGRect) { + guard let params = self.params else { + return + } + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(rect) + + context.setFillColor(UIColor.black.cgColor) + for item in params.items { + context.fillEllipse(in: CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter))) + } + + context.setFillColor(UIColor.clear.cgColor) + for item in params.items { + context.fillEllipse(in: CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter)).insetBy(dx: params.lineWidth, dy: params.lineWidth)) + } + + context.setBlendMode(.sourceIn) + let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map { + UIColor(rgb: $0).cgColor + } as CFArray, locations: nil)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 50.0), options: []) + + context.setBlendMode(.copy) + + for i in (0 ..< params.items.count).reversed() { + let item = params.items[i] + if i != params.items.count - 1 { + let previousItem = params.items[i] + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: previousItem.position.x - previousItem.diameter * 0.5, y: previousItem.position.y - previousItem.diameter * 0.5), size: CGSize(width: previousItem.diameter, height: previousItem.diameter)).insetBy(dx: params.lineWidth, dy: params.lineWidth)) + } + + context.setBlendMode(.normal) + + let imageRect = CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter)).insetBy(dx: params.lineWidth + params.innerSpacing, dy: params.lineWidth + params.innerSpacing) + + if let image = item.image { + context.draw(image.cgImage!, in: imageRect) + } else { + context.setFillColor(UIColor.black.cgColor) + context.fillEllipse(in: imageRect) + } + } + } +} + +public final class StorySetIndicatorComponent: Component { + public let context: AccountContext + public let peer: EnginePeer + public let items: [EngineStoryItem] + public let hasUnseen: Bool + public let totalCount: Int + public let theme: PresentationTheme + public let action: () -> Void + + public init( + context: AccountContext, + peer: EnginePeer, + items: [EngineStoryItem], + hasUnseen: Bool, + totalCount: Int, + theme: PresentationTheme, + action: @escaping () -> Void + ) { + self.context = context + self.peer = peer + self.items = items + self.hasUnseen = hasUnseen + self.totalCount = totalCount + self.theme = theme + self.action = action + } + + public static func ==(lhs: StorySetIndicatorComponent, rhs: StorySetIndicatorComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.hasUnseen != rhs.hasUnseen { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + private final class ImageContext { + private var fetchDisposable: Disposable? + private var imageDisposable: Disposable? + private let updated: () -> Void + + private(set) var image: UIImage? + + init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem, updated: @escaping () -> Void) { + self.updated = updated + + let peerReference = PeerReference(peer._asPeer()) + + var messageMedia: EngineMedia? + switch item.media { + case let .image(image): + messageMedia = .image(image) + case let .file(file): + messageMedia = .file(file) + default: + break + } + + let reloadMedia = true + + if reloadMedia, let messageMedia, let peerReference { + var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var fetchSignal: Signal? + switch messageMedia { + case let .image(image): + signal = chatMessagePhoto( + postbox: context.account.postbox, + userLocation: .other, + photoReference: .story(peer: peerReference, id: item.id, media: image), + synchronousLoad: false, + highQuality: true + ) + if let representation = image.representations.last { + fetchSignal = fetchedMediaResource( + mediaBox: context.account.postbox.mediaBox, + userLocation: .other, + userContentType: .image, + reference: ImageMediaReference.story(peer: peerReference, id: item.id, media: image).resourceReference(representation.resource) + ) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } + case let .file(file): + signal = mediaGridMessageVideo( + postbox: context.account.postbox, + userLocation: .other, + videoReference: .story(peer: peerReference, id: item.id, media: file), + onlyFullSize: false, + useLargeThumbnail: true, + synchronousLoad: false, + autoFetchFullSizeThumbnail: true, + overlayColor: nil, + nilForEmptyResult: false, + useMiniThumbnailIfAvailable: false, + blurred: false + ) + fetchSignal = fetchedMediaResource( + mediaBox: context.account.postbox.mediaBox, + userLocation: .other, + userContentType: .image, + reference: FileMediaReference.story(peer: peerReference, id: item.id, media: file).resourceReference(file.resource) + ) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + default: + break + } + + if let signal { + var wasSynchronous = true + self.imageDisposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] process in + guard let self else { + return + } + + let outerSize = CGSize(width: 1080.0, height: 1920.0) + let innerSize = CGSize(width: 26.0, height: 26.0) + + let result = process(TransformImageArguments(corners: ImageCorners(radius: innerSize.width * 0.5), imageSize: outerSize.aspectFilled(innerSize), boundingSize: innerSize, intrinsicInsets: UIEdgeInsets())) + if let result { + self.image = result.generateImage() + if !wasSynchronous { + self.updated() + } + } + }) + wasSynchronous = false + } + + self.fetchDisposable?.dispose() + self.fetchDisposable = nil + if let fetchSignal { + self.fetchDisposable = (fetchSignal |> deliverOnMainQueue).start() + } + } + } + + deinit { + self.fetchDisposable?.dispose() + self.imageDisposable?.dispose() + } + } + + public final class View: UIView { + private let button: HighlightTrackingButton + private let imageView: ShapeImageView + private let text = ComponentView() + + private var imageContexts: [Int32: ImageContext] = [:] + + private var component: StorySetIndicatorComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.button = HighlightTrackingButton() + + self.imageView = ShapeImageView(frame: CGRect()) + self.imageView.isUserInteractionEnabled = false + self.imageView.backgroundColor = nil + self.imageView.isOpaque = false + + super.init(frame: frame) + + self.button.addSubview(self.imageView) + self.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.button.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + let transition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut)) + transition.setSublayerTransform(view: self.button, transform: CATransform3DMakeScale(0.8, 0.8, 1.0)) + } else { + let transition = Transition(animation: .curve(duration: 0.24, curve: .easeInOut)) + transition.setSublayerTransform(view: self.button, transform: CATransform3DIdentity) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action() + } + + func update(component: StorySetIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let innerDiameter: CGFloat = 26.0 + let innerSpacing: CGFloat = 1.33 + let lineWidth: CGFloat = 1.33 + let outerDiameter: CGFloat = innerDiameter + innerSpacing * 2.0 + lineWidth * 2.0 + let overflow: CGFloat = 14.0 + + var validIds: [Int32] = [] + var items: [ShapeImageView.Item] = [] + for i in 0 ..< min(3, component.items.count) { + validIds.append(component.items[i].id) + + let imageContext: ImageContext + if let current = self.imageContexts[component.items[i].id] { + imageContext = current + } else { + var update = false + imageContext = ImageContext(context: component.context, peer: component.peer, item: component.items[i], updated: { [weak self] in + guard let self else { + return + } + if update { + self.state?.updated(transition: .immediate) + } + }) + self.imageContexts[component.items[i].id] = imageContext + update = true + } + + items.append(ShapeImageView.Item( + position: CGPoint(x: outerDiameter * 0.5 + CGFloat(i) * (outerDiameter - overflow), y: outerDiameter * 0.5), + diameter: outerDiameter, + image: imageContext.image + )) + } + + var removeIds: [Int32] = [] + for (id, _) in self.imageContexts { + if !validIds.contains(id) { + removeIds.append(id) + } + } + for id in removeIds { + self.imageContexts.removeValue(forKey: id) + } + + let maxItemsWidth: CGFloat = outerDiameter * 0.5 + CGFloat(max(0, 3 - 1)) * (outerDiameter - overflow) + outerDiameter * 0.5 + let effectiveItemsWidth: CGFloat = outerDiameter * 0.5 + CGFloat(max(0, items.count - 1)) * (outerDiameter - overflow) + outerDiameter * 0.5 + + let borderColors: [UInt32] + if component.theme.overallDarkAppearance { + if component.hasUnseen { + borderColors = [ + 0x34C76F, + 0x3DA1FD + ] + } else { + borderColors = [ + 0x48484A, + 0x48484A + ] + } + } else { + if component.hasUnseen { + borderColors = [ + 0x34C76F, + 0x3DA1FD + ] + } else { + borderColors = [ + 0xD8D8E1, + 0xD8D8E1 + ] + } + } + + let imageSize = CGSize(width: maxItemsWidth, height: outerDiameter) + let params = ShapeImageView.Params( + items: items, + innerSpacing: innerSpacing, + lineWidth: lineWidth, + borderColors: borderColors + ) + if self.imageView.params != params || self.imageView.bounds.size != imageSize { + self.imageView.params = params + self.imageView.frame = CGRect(origin: CGPoint(), size: imageSize) + self.imageView.setNeedsDisplay() + } + + //TODO:localize + let textValue: String + if component.totalCount == 0 { + textValue = "" + } else if component.totalCount == 1 { + textValue = "1 story" + } else { + textValue = "\(component.totalCount) stories" + } + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(Text(text: textValue, font: Font.semibold(17.0), color: .white)), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + let textFrame = CGRect(origin: CGPoint(x: effectiveItemsWidth + 6.0, y: 5.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + textView.isUserInteractionEnabled = false + self.button.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.origin) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + let size = CGSize(width: effectiveItemsWidth + 6.0 + textSize.width, height: outerDiameter) + transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index c932bd4a69..2560d71d23 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2136,6 +2136,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if message.associatedStories[story.storyId] == nil { storiesRequiredValidation = true } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let story = content.story { + if message.associatedStories[story.storyId] == nil { + storiesRequiredValidation = true + } } } if contentRequiredValidation { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index b0f69200ca..76fafa5c08 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -503,6 +503,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { isImage = true } else if let _ = media as? WallpaperPreviewMedia { isImage = true + } else if let _ = media as? TelegramMediaStory { + isImage = true } } @@ -677,6 +679,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { skipStandardStatus = true } + } else if let story = media as? TelegramMediaStory { + var media: Media? + if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue { + media = item.media + } + + var automaticDownload = false + if let media { + automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media) + } + + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout } } diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index ab5d95c40e..4dc8b57d59 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -164,12 +164,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { switch type { case .instagram, .twitter: if automaticPlayback { - mainMedia = webpage.file ?? webpage.image + mainMedia = webpage.story ?? webpage.file ?? webpage.image } else { - mainMedia = webpage.image ?? webpage.file + mainMedia = webpage.story ?? webpage.image ?? webpage.file } default: - mainMedia = webpage.file ?? webpage.image + mainMedia = webpage.story ?? webpage.file ?? webpage.image } let themeMimeType = "application/x-tgtheme-ios" @@ -216,6 +216,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } mediaAndFlags = (image, flags) } + } else if let story = mainMedia as? TelegramMediaStory { + mediaAndFlags = (story, []) } else if let type = webpage.type { if type == "telegram_background" { var colors: [UInt32] = [] @@ -338,7 +340,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } for media in item.message.media { switch media { - case _ as TelegramMediaImage, _ as TelegramMediaFile: + case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaStory: mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags()) default: break diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 888c1da692..39dfe3d449 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -26,32 +26,39 @@ import StoryContainerScreen import StoryContentComponent func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { + var story: TelegramMediaStory? for media in params.message.media { if let media = media as? TelegramMediaStory { - let navigationController = params.navigationController - let context = params.context - let storyContent = SingleStoryContentContextImpl(context: params.context, storyId: media.storyId) - let _ = (storyContent.state - |> take(1) - |> deliverOnMainQueue).start(next: { [weak navigationController] _ in - let transitionIn: StoryContainerScreen.TransitionIn? = nil - - let storyContainerScreen = StoryContainerScreen( - context: context, - content: storyContent, - transitionIn: transitionIn, - transitionOut: { _, _ in - let transitionOut: StoryContainerScreen.TransitionOut? = nil - - return transitionOut - } - ) - navigationController?.pushViewController(storyContainerScreen) - }) - return true + story = media + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.story != nil { + story = content.story } } + if let story { + let navigationController = params.navigationController + let context = params.context + let storyContent = SingleStoryContentContextImpl(context: params.context, storyId: story.storyId) + let _ = (storyContent.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak navigationController] _ in + let transitionIn: StoryContainerScreen.TransitionIn? = nil + + let storyContainerScreen = StoryContainerScreen( + context: context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { _, _ in + let transitionOut: StoryContainerScreen.TransitionOut? = nil + + return transitionOut + } + ) + navigationController?.pushViewController(storyContainerScreen) + }) + return true + } + if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) { switch mediaData { case let .url(url): diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 80d8d560c5..c91f293386 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -438,8 +438,6 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer) - - self.updateStoryView(transition: .immediate) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { @@ -455,7 +453,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.playbackStartDisposable.dispose() } - func updateStoryView(transition: ContainedViewLayoutTransition) { + func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { if let hasUnseenStories = self.hasUnseenStories { let avatarStoryView: ComponentView if let current = self.avatarStoryView { @@ -468,7 +466,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let _ = avatarStoryView.update( transition: Transition(transition), component: AnyComponent(AvatarStoryIndicatorComponent( - hasUnseen: hasUnseenStories + hasUnseen: hasUnseenStories, + isDarkTheme: theme.overallDarkAppearance )), environment: {}, containerSize: self.avatarNode.bounds.size @@ -783,6 +782,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } } } + + self.updateStoryView(transition: .immediate, theme: theme) } } @@ -1161,6 +1162,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? var animateOverlaysFadeIn: (() -> Void)? + var openStories: (() -> Void)? init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { self.isSettings = isSettings @@ -1250,6 +1252,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode { } strongSelf.animateOverlaysFadeIn?() } + + self.listContainerNode.openStories = { [weak self] in + guard let self else { + return + } + self.openStories?() + } } func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { @@ -1591,7 +1600,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable { let isForExpandedView: Bool } -final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { +final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { private var presentationData: PresentationData? private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index a9a0dee67a..be5be12c0a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3044,54 +3044,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - if !gallery, let expiringStoryList = strongSelf.expiringStoryList, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty { - let _ = expiringStoryList - let storyContent = StoryContentContextImpl(context: strongSelf.context, includeHidden: false, focusedPeerId: strongSelf.peerId, singlePeer: true) - let _ = (storyContent.state - |> take(1) - |> deliverOnMainQueue).start(next: { storyContentState in - guard let self else { - return - } - var transitionIn: StoryContainerScreen.TransitionIn? - transitionIn = nil - - let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view - transitionIn = StoryContainerScreen.TransitionIn( - sourceView: transitionView, - sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 - ) - - self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = true - - let storyContainerScreen = StoryContainerScreen( - context: self.context, - content: storyContent, - transitionIn: transitionIn, - transitionOut: { [weak self] peerId, _ in - guard let self else { - return nil - } - - let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view - return StoryContainerScreen.TransitionOut( - destinationView: transitionView, - transitionView: nil, - destinationRect: transitionView.bounds, - destinationCornerRadius: transitionView.bounds.height * 0.5, - destinationIsAvatar: true, - completed: { [weak self] in - guard let self else { - return - } - self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = false - } - ) - } - ) - self.controller?.push(storyContainerScreen) - }) + if !gallery, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty { + strongSelf.openStories(fromAvatar: true) return } @@ -3913,18 +3867,40 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else if peerId.namespace == Namespaces.Peer.CloudUser { let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) self.expiringStoryList = expiringStoryList - self.expiringStoryListDisposable = (expiringStoryList.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self else { + self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + expiringStoryList.state + ) + |> deliverOnMainQueue).start(next: { [weak self] peer, state in + guard let self, let peer else { return } self.expiringStoryListState = state if state.items.isEmpty { self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = nil + self.headerNode.avatarListNode.listContainerNode.storyParams = nil } else { self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = state.hasUnseen + self.headerNode.avatarListNode.listContainerNode.storyParams = (peer, state.items.prefix(3).compactMap { item -> EngineStoryItem? in + switch item { + case let .item(item): + return item + case .placeholder: + return nil + } + }, state.items.count, state.hasUnseen) + } + + self.requestLayout(animated: false) + + if self.headerNode.avatarListNode.openStories == nil { + self.headerNode.avatarListNode.openStories = { [weak self] in + guard let self else { + return + } + self.openStories(fromAvatar: false) + } } - self.headerNode.avatarListNode.avatarContainerNode.updateStoryView(transition: .immediate) }) } } @@ -4115,6 +4091,64 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil) } + private func openStories(fromAvatar: Bool) { + if let expiringStoryList = self.expiringStoryList, let expiringStoryListState = self.expiringStoryListState, !expiringStoryListState.items.isEmpty { + let _ = expiringStoryList + let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: self.peerId, singlePeer: true) + let _ = (storyContent.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] storyContentState in + guard let self else { + return + } + var transitionIn: StoryContainerScreen.TransitionIn? + + if fromAvatar { + let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: transitionView, + sourceRect: transitionView.bounds, + sourceCornerRadius: transitionView.bounds.height * 0.5 + ) + } + + self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = true + + let storyContainerScreen = StoryContainerScreen( + context: self.context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { [weak self] peerId, _ in + guard let self else { + return nil + } + if !fromAvatar { + return nil + } + + let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view + return StoryContainerScreen.TransitionOut( + destinationView: transitionView, + transitionView: nil, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5, + destinationIsAvatar: true, + completed: { [weak self] in + guard let self else { + return + } + self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = false + } + ) + } + ) + self.controller?.push(storyContainerScreen) + }) + + return + } + } + private func openMessage(id: MessageId) -> Bool { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { return false