diff --git a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift index 2cf9442d99..56120b515e 100644 --- a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift @@ -75,6 +75,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { super.init(target: target, action: action) self.maximumNumberOfTouches = 1 + self.delaysTouchesBegan = false } override public func reset() { @@ -136,7 +137,9 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { let size = self.view?.bounds.size ?? CGSize() - print("moved: \(CFAbsoluteTimeGetCurrent()) absTranslationX: \(absTranslationX) absTranslationY: \(absTranslationY)") + //print("moved: \(CFAbsoluteTimeGetCurrent()) absTranslationX: \(absTranslationX) absTranslationY: \(absTranslationY)") + + var fireBegan = false if self.currentAllowedDirections.contains(.down) { if !self.validatedGesture { @@ -177,12 +180,18 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { self.state = .failed } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { self.validatedGesture = true + fireBegan = true } } } if self.validatedGesture { super.touchesMoved(touches, with: event) + if fireBegan { + if self.state == .possible { + self.state = .began + } + } } } } diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 0c2ec4473c..59be617288 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -332,7 +332,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) self.ignoreScrolling = true - self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled + self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat let previousBounds = self.scrollNode.bounds let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size) self.scrollNode.frame = scrollNodeFrame @@ -348,7 +348,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes } self.ignoreScrolling = false - self.scrollNode.view.isScrollEnabled = !isStandaloneModal + self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat let isLandscape = layout.orientation == .landscape let containerLayout: ContainerViewLayout @@ -515,6 +515,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) { return self.dim.view } + if self.isFlat { + return result + } var currentParent: UIView? = result var enableScrolling = true while true { @@ -562,7 +565,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes enableScrolling = false } } - self.scrollNode.view.isScrollEnabled = enableScrolling + self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat return result } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 0d87fe12f6..e6b2828e23 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1669,6 +1669,56 @@ public final class EngineStoryViewListContext { } } + if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited + )) + if let entry = CodableEntry(updatedItem) { + transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) + } + } + + var currentItems = transaction.getStoryItems(peerId: account.peerId) + for i in 0 ..< currentItems.count { + if currentItems[i].id == storyId { + if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited + )) + if let entry = CodableEntry(updatedItem) { + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) + } + } + } + } + transaction.setStoryItems(peerId: account.peerId, items: currentItems) + return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset) case .none: return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil) diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index 807efc98a9..f5842622ea 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/AppBundle", "//submodules/PeerPresenceStatusManager", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index ab0bbd11c6..563fd3d1c9 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -13,6 +13,7 @@ import CheckNode import TelegramStringFormatting import AppBundle import PeerPresenceStatusManager +import EmojiStatusComponent private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -132,6 +133,7 @@ public final class PeerListItemComponent: Component { private let label = ComponentView() private let separatorLayer: SimpleLayer private let avatarNode: AvatarNode + private var avatarIcon: ComponentView? private var iconView: UIImageView? private var checkLayer: CheckLayer? @@ -316,6 +318,8 @@ public final class PeerListItemComponent: Component { } else { transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) } + + var statusIcon: EmojiStatusComponent.Content? if let peer = component.peer { let clipStyle: AvatarNodeClipStyle if case let .channel(channel) = peer, channel.flags.contains(.isForum) { @@ -324,6 +328,18 @@ public final class PeerListItemComponent: Component { clipStyle = .round } self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + + if peer.isScam { + statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) + } else if peer.isFake { + statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased()) + } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) + } else if peer.isVerified { + statusIcon = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + } else if peer.isPremium { + statusIcon = .premium(color: component.theme.list.itemAccentColor) + } } let labelSize = self.label.update( @@ -350,7 +366,7 @@ public final class PeerListItemComponent: Component { containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) - let titleSpacing: CGFloat = 1.0 + let titleSpacing: CGFloat = 2.0 let centralContentHeight: CGFloat if labelSize.height > 0.0, case .generic = component.style { centralContentHeight = titleSize.height + labelSize.height + titleSpacing @@ -358,7 +374,7 @@ public final class PeerListItemComponent: Component { centralContentHeight = titleSize.height } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -419,6 +435,48 @@ public final class PeerListItemComponent: Component { transition.setFrame(view: labelView, frame: labelFrame) } + if let statusIcon { + let animationCache = component.context.animationCache + let animationRenderer = component.context.animationRenderer + + let avatarIcon: ComponentView + var avatarIconTransition = transition + if let current = self.avatarIcon { + avatarIcon = current + } else { + avatarIconTransition = transition.withAnimation(.none) + avatarIcon = ComponentView() + self.avatarIcon = avatarIcon + } + + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: statusIcon, + isVisibleForAnimations: true, + action: nil, + emojiFileUpdated: nil + ) + let iconSize = avatarIcon.update( + transition: avatarIconTransition, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + if let avatarIconView = avatarIcon.view { + if avatarIconView.superview == nil { + avatarIconView.isUserInteractionEnabled = false + self.containerButton.addSubview(avatarIconView) + } + avatarIconTransition.setFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + } + } else if let avatarIcon = self.avatarIcon { + self.avatarIcon = nil + avatarIcon.view?.removeFromSuperview() + } + if themeUpdated { self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 31f452f277..e45a0c84e7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -72,7 +72,7 @@ swift_library( "//submodules/TinyThumbnail", "//submodules/ImageBlur", "//submodules/StickerPackPreviewUI", - + "//submodules/Components/AnimatedStickerComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index da21e1214b..dcc296e21b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -398,7 +398,7 @@ public final class StoryContentContextImpl: StoryContentContext { private var requestStoryDisposables = DisposableSet() private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:] - private var pollStoryMetadataDisposables = DisposableSet() + private var pollStoryMetadataDisposables: [StoryId: Disposable] = [:] private var singlePeerListContext: PeerExpiringStoryListContext? @@ -615,7 +615,9 @@ public final class StoryContentContextImpl: StoryContentContext { for (_, disposable) in self.preloadStoryResourceDisposables { disposable.dispose() } - self.pollStoryMetadataDisposables.dispose() + for (_, disposable) in self.pollStoryMetadataDisposables { + disposable.dispose() + } self.storySubscriptionsDisposable?.dispose() } @@ -805,7 +807,11 @@ public final class StoryContentContextImpl: StoryContentContext { } } for (peerId, ids) in pollIdByPeerId { - self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start()) + for id in ids { + if self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] == nil { + self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] = self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start() + } + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 7dc185ed17..b6765eeeb0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -33,6 +33,7 @@ func hasFirstResponder(_ view: UIView) -> Bool { } private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { + var shouldBegin: ((UITouch) -> Bool)? var updateIsTracking: ((Bool) -> Void)? override var state: UIGestureRecognizer.State { @@ -50,10 +51,12 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { } private var isTracking: Bool = false + private var isValidated: Bool = false override func reset() { super.reset() + self.isValidated = false if self.isTracking { self.isTracking = false self.updateIsTracking?(false) @@ -61,11 +64,21 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { } override func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) + if !self.isValidated, let touch = touches.first { + if let shouldBegin = self.shouldBegin, shouldBegin(touch) { + self.isValidated = true + } else { + return + } + } - if !self.isTracking { - self.isTracking = true - self.updateIsTracking?(true) + if self.isValidated { + super.touchesBegan(touches, with: event) + + if !self.isTracking { + self.isTracking = true + self.updateIsTracking?(true) + } } } } @@ -153,9 +166,8 @@ private final class StoryContainerScreenComponent: Component { self.didBegin = didBegin } } - - final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + final class View: UIView, UIGestureRecognizerDelegate { private var component: StoryContainerScreenComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? @@ -190,6 +202,8 @@ private final class StoryContainerScreenComponent: Component { var dismissWithoutTransitionOut: Bool = false + var longPressRecognizer: StoryLongPressRecognizer? + override init(frame: CGRect) { self.backgroundLayer = SimpleLayer() self.backgroundLayer.backgroundColor = UIColor.black.cgColor @@ -236,7 +250,7 @@ private final class StoryContainerScreenComponent: Component { return [] } } - if !itemSetComponentView.allowsInteractiveGestures() { + if !itemSetComponentView.allowsVerticalPanGesture() { return [] } @@ -253,6 +267,19 @@ private final class StoryContainerScreenComponent: Component { self.isHoldingTouch = isTracking self.state?.updated(transition: .immediate) } + longPressRecognizer.shouldBegin = { [weak self] touch in + guard let self else { + return false + } + guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + return false + } + if !itemSetComponentView.isPointInsideContentArea(point: touch.location(in: itemSetComponentView)) { + return false + } + return true + } + self.longPressRecognizer = longPressRecognizer self.addGestureRecognizer(longPressRecognizer) let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) @@ -406,7 +433,7 @@ private final class StoryContainerScreenComponent: Component { @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: - print("began: \(CFAbsoluteTimeGetCurrent())") + //print("began: \(CFAbsoluteTimeGetCurrent())") self.beginHorizontalPan(translation: recognizer.translation(in: self)) case .changed: self.updateHorizontalPan(translation: recognizer.translation(in: self)) @@ -450,7 +477,7 @@ private final class StoryContainerScreenComponent: Component { self.verticalPanState = nil var updateState = true - if translation.y > 200.0 || (translation.y > 100.0 && velocity.y > 200.0) { + if translation.y > 200.0 || (translation.y > 5.0 && velocity.y > 200.0) { self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) self.environment?.controller()?.dismiss() } else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 461ad29526..8f367f54b6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -218,7 +218,7 @@ final class StoryContentCaptionComponent: Component { let edgeDistanceFraction = edgeDistance / 7.0 transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction) - let shadowOverflow: CGFloat = 36.0 + let shadowOverflow: CGFloat = 56.0 let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame) transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0))) @@ -479,7 +479,7 @@ final class StoryContentCaptionComponent: Component { var locations: [NSNumber] = [] var colors: [CGColor] = [] let numStops = 10 - let baseAlpha: CGFloat = 0.3 + let baseAlpha: CGFloat = 0.5 for i in 0 ..< numStops { let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1) locations.append((1.0 - step) as NSNumber) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 493f0f2743..58b3916ae8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -410,10 +410,8 @@ final class StoryItemContentComponent: Component { var fetchSignal: Signal? switch messageMedia { case .image: - self.contentLoaded = true + break case let .file(file): - self.contentLoaded = true - fetchSignal = fetchedMediaResource( mediaBox: component.context.account.postbox.mediaBox, userLocation: .other, @@ -446,6 +444,16 @@ final class StoryItemContentComponent: Component { } if let messageMedia { + var applyState = false + self.imageView.didLoadContents = { [weak self] in + guard let self else { + return + } + self.contentLoaded = true + if applyState { + self.state?.updated(transition: .immediate) + } + } self.imageView.update( context: component.context, peer: component.peer, @@ -456,6 +464,10 @@ final class StoryItemContentComponent: Component { attemptSynchronous: synchronousLoad, transition: transition ) + applyState = true + if self.imageView.isContentLoaded { + self.contentLoaded = true + } transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize)) var dimensions: CGSize? diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index fa65152e40..bc0a3602dd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -16,6 +16,9 @@ final class StoryItemImageView: UIView { private var disposable: Disposable? private var fetchDisposable: Disposable? + private(set) var isContentLoaded: Bool = false + var didLoadContents: (() -> Void)? + override init(frame: CGRect) { self.contentView = UIImageView() self.contentView.contentMode = .scaleAspectFill @@ -55,6 +58,8 @@ final class StoryItemImageView: UIView { self.updateImage(image: image) } } + self.isContentLoaded = true + self.didLoadContents?() } else { if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) @@ -89,6 +94,8 @@ final class StoryItemImageView: UIView { } if let image { self.updateImage(image: image) + self.isContentLoaded = true + self.didLoadContents?() } }) } @@ -110,6 +117,8 @@ final class StoryItemImageView: UIView { self.updateImage(image: image) } } + self.isContentLoaded = true + self.didLoadContents?() } else { if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) @@ -141,6 +150,8 @@ final class StoryItemImageView: UIView { } if let image { self.updateImage(image: image) + self.isContentLoaded = true + self.didLoadContents?() } }) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 7e92323b2e..f1d19537b5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -269,6 +269,14 @@ public final class StoryItemSetContainerComponent: Component { } } + private struct PanState: Equatable { + var fraction: CGFloat + + init(fraction: CGFloat) { + self.fraction = fraction + } + } + private final class Scroller: UIScrollView { override init(frame: CGRect) { super.init(frame: frame) @@ -319,7 +327,7 @@ public final class StoryItemSetContainerComponent: Component { var preparingToDisplayViewList: Bool = false var displayViewList: Bool = false - var viewList: ViewList? + var viewLists: [Int32: ViewList] = [:] var isEditingStory: Bool = false @@ -354,6 +362,11 @@ public final class StoryItemSetContainerComponent: Component { private var animateNextNavigationId: Int32? private var initializedOffset: Bool = false + private var viewListPanState: PanState? + private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer? + + private var verticalPanState: PanState? + override init(frame: CGRect) { self.sendMessageContext = StoryItemSetContainerSendMessage() @@ -365,6 +378,7 @@ public final class StoryItemSetContainerComponent: Component { self.scroller.showsVerticalScrollIndicator = false self.scroller.showsHorizontalScrollIndicator = false self.scroller.decelerationRate = .fast + self.scroller.delaysContentTouches = false self.controlsContainerView = SparseContainerView() self.controlsContainerView.clipsToBounds = true @@ -404,6 +418,33 @@ public final class StoryItemSetContainerComponent: Component { tapRecognizer.delegate = self self.itemsContainerView.addGestureRecognizer(tapRecognizer) + let verticalPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.viewListDismissPanGesture(_:)), allowedDirections: { [weak self] point in + guard let self else { + return [] + } + if !self.displayViewList { + return [] + } + return [.down] + }) + self.addGestureRecognizer(verticalPanRecognizer) + + let viewListSwipeRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.viewListPanGesture(_:)), allowedDirections: { [weak self] point in + guard let self else { + return [] + } + if !self.displayViewList { + return [] + } + if self.bounds.contains(point), !self.itemsContainerView.frame.contains(point) { + return [.left, .right] + } else { + return [] + } + }) + self.viewListSwipeRecognizer = viewListSwipeRecognizer + self.addGestureRecognizer(viewListSwipeRecognizer) + self.audioRecorderDisposable = (self.sendMessageContext.audioRecorder.get() |> deliverOnMainQueue).start(next: { [weak self] audioRecorder in guard let self else { @@ -531,6 +572,13 @@ public final class StoryItemSetContainerComponent: Component { return true } + func allowsVerticalPanGesture() -> Bool { + if self.displayViewList { + return false + } + return true + } + func rewindCurrentItem() { guard let component = self.component else { return @@ -623,6 +671,78 @@ public final class StoryItemSetContainerComponent: Component { } } + @objc private func viewListPanGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + if !self.bounds.isEmpty { + let translation = recognizer.translation(in: self) + let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width)) + self.viewListPanState = PanState(fraction: fraction) + self.state?.updated(transition: .immediate) + } + case .changed: + if var viewListPanState = self.viewListPanState { + let translation = recognizer.translation(in: self) + let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width)) + viewListPanState.fraction = fraction + self.viewListPanState = viewListPanState + self.state?.updated(transition: .immediate) + } + case .cancelled, .ended: + if let viewListPanState = self.viewListPanState { + let velocity = recognizer.velocity(in: self) + + var consumed = false + if let component = self.component, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if (viewListPanState.fraction <= -0.3 || (viewListPanState.fraction <= -0.05 && velocity.x <= -200.0)), currentIndex != component.slice.allItems.count - 1 { + let nextItem = component.slice.allItems[currentIndex + 1] + self.animateNextNavigationId = nextItem.storyItem.id + component.navigate(.id(nextItem.storyItem.id)) + consumed = true + } else if (viewListPanState.fraction >= 0.3 || (viewListPanState.fraction >= 0.05 && velocity.x >= 200.0)), currentIndex != 0 { + let previousItem = component.slice.allItems[currentIndex - 1] + self.animateNextNavigationId = previousItem.storyItem.id + component.navigate(.id(previousItem.storyItem.id)) + consumed = true + } + } + + if !consumed { + self.viewListPanState = nil + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + } + default: + break + } + } + + @objc private func viewListDismissPanGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.verticalPanState = PanState(fraction: 0.0) + self.state?.updated(transition: .immediate) + case .changed: + let translation = recognizer.translation(in: self) + self.verticalPanState = PanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height))) + self.state?.updated(transition: .immediate) + case .cancelled, .ended: + if let verticalPanState = self.verticalPanState { + self.verticalPanState = nil + + let velocity = recognizer.velocity(in: self) + + if verticalPanState.fraction >= 0.3 || (verticalPanState.fraction >= 0.05 && velocity.y >= 150.0) { + self.displayViewList = false + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + default: + break + } + } + @objc private func closePressed() { guard let component = self.component else { return @@ -814,7 +934,11 @@ public final class StoryItemSetContainerComponent: Component { for index in 0 ..< component.slice.allItems.count { let item = component.slice.allItems[index] - let offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance + var offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance + if let viewListPanState = self.viewListPanState { + offsetFraction += viewListPanState.fraction + } + let centerIndexOffset = index - centralIndex let centerFraction: CGFloat = CGFloat(centerIndexOffset) @@ -996,7 +1120,7 @@ public final class StoryItemSetContainerComponent: Component { return false } if component.slice.peer.id == component.context.account.peerId { - if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { + if let _ = component.slice.item.storyItem.views { self.displayViewList = true if component.verticalPanFraction == 0.0 { self.preparingToDisplayViewList = true @@ -1062,15 +1186,17 @@ public final class StoryItemSetContainerComponent: Component { ) inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28) } - if let viewListView = self.viewList?.view.view { - viewListView.layer.animatePosition( - from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY), - to: CGPoint(), - duration: 0.3, - timingFunction: kCAMediaTimingFunctionSpring, - additive: true - ) - viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28) + for (_, viewList) in self.viewLists { + if let viewListView = viewList.view.view { + viewListView.layer.animatePosition( + from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.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( @@ -1177,16 +1303,18 @@ public final class StoryItemSetContainerComponent: Component { ) inputPanelBackground.layer.animateAlpha(from: inputPanelBackground.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.controlsContainerView.frame.maxY), - duration: 0.3, - timingFunction: kCAMediaTimingFunctionSpring, - removeOnCompletion: false, - additive: true - ) - viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false) + for (_, viewList) in self.viewLists { + if let viewListView = viewList.view.view { + viewListView.layer.animatePosition( + from: CGPoint(), + to: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY), + duration: 0.3, + timingFunction: kCAMediaTimingFunctionSpring, + removeOnCompletion: false, + additive: true + ) + viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false) + } } if let captionItemView = self.captionItem?.view.view { captionItemView.layer.animatePosition( @@ -1439,12 +1567,12 @@ public final class StoryItemSetContainerComponent: Component { } if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id { - component.markAsSeen(StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)) self.initializedOffset = false } var itemsTransition = transition if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id { self.animateNextNavigationId = nil + self.viewListPanState = nil itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) } @@ -1705,144 +1833,213 @@ public final class StoryItemSetContainerComponent: Component { inputPanelIsOverlay = true } - if component.slice.peer.id == component.context.account.peerId { - let viewList: ViewList - var viewListTransition = transition - if let current = self.viewList { - viewList = current - } else { - if !transition.animation.isImmediate { - viewListTransition = .immediate - } - viewList = ViewList() - self.viewList = viewList - } - - let outerExpansionFraction: CGFloat + var validViewListIds: [Int32] = [] + if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id] if self.displayViewList { - outerExpansionFraction = 1.0 - } else if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { - outerExpansionFraction = component.verticalPanFraction - } else { - outerExpansionFraction = 0.0 + if currentIndex != 0 { + visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id) + } + if currentIndex != component.slice.allItems.count - 1 { + visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id) + } } - viewList.view.parentState = state - let viewListSize = viewList.view.update( - transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint( - synchronousLoad: false - )).withUserData(StoryItemSetViewListComponent.AnimationHint( - synchronous: false - )), - component: AnyComponent(StoryItemSetViewListComponent( - externalState: viewList.externalState, - context: component.context, - theme: component.theme, - strings: component.strings, - sharedListsContext: component.sharedViewListsContext, - peerId: component.slice.peer.id, - safeInsets: component.safeInsets, - storyItem: component.slice.item.storyItem, - 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.preparingToDisplayViewList = true - self.updateScrolling(transition: .immediate) - self.preparingToDisplayViewList = false - - 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 Story", color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component else { - return - } - component.delete() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - - actionSheet.dismissed = { [weak self] _ in + var viewListBaseOffsetX: CGFloat = 0.0 + if let viewListPanState = self.viewListPanState { + viewListBaseOffsetX = viewListPanState.fraction * availableSize.width + } + + var fixedAnimationOffset: CGFloat = 0.0 + var applyFixedAnimationOffsetIds: [Int32] = [] + + for id in visibleViewListIds { + guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else { + continue + } + let item = component.slice.allItems[itemIndex] + validViewListIds.append(id) + + let viewList: ViewList + var viewListTransition = itemsTransition + if let current = self.viewLists[id] { + viewList = current + } else { + if !itemsTransition.animation.isImmediate { + viewListTransition = .immediate + } + viewList = ViewList() + self.viewLists[id] = viewList + applyFixedAnimationOffsetIds.append(id) + } + + let outerExpansionFraction: CGFloat + let outerExpansionDirection: Bool + if self.displayViewList { + if let verticalPanState = self.verticalPanState { + outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction)) + } else { + outerExpansionFraction = 1.0 + } + outerExpansionDirection = false + } else if let _ = item.storyItem.views { + outerExpansionFraction = component.verticalPanFraction + outerExpansionDirection = true + } else { + outerExpansionFraction = 0.0 + outerExpansionDirection = true + } + + viewList.view.parentState = state + let viewListSize = viewList.view.update( + transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint( + synchronousLoad: false + )).withUserData(StoryItemSetViewListComponent.AnimationHint( + synchronous: false + )), + component: AnyComponent(StoryItemSetViewListComponent( + externalState: viewList.externalState, + context: component.context, + theme: component.theme, + strings: component.strings, + sharedListsContext: component.sharedViewListsContext, + peerId: component.slice.peer.id, + safeInsets: component.safeInsets, + storyItem: item.storyItem, + outerExpansionFraction: outerExpansionFraction, + outerExpansionDirection: outerExpansionDirection, + close: { [weak self] in guard let self else { return } - self.sendMessageContext.actionSheet = nil + 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.preparingToDisplayViewList = true + self.updateScrolling(transition: .immediate) + self.preparingToDisplayViewList = false + + 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 Story", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component else { + return + } + component.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.sendMessageContext.actionSheet = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.actionSheet = actionSheet self.updateIsProgressPaused() + + component.presentController(actionSheet, nil) + }, + moreAction: { [weak self] sourceView, gesture in + guard let self else { + return + } + self.performMoreAction(sourceView: sourceView, gesture: gesture) + }, + openPeer: { [weak self] peer in + guard let self else { + return + } + self.navigateToPeer(peer: peer, chat: false) } - self.sendMessageContext.actionSheet = actionSheet - self.updateIsProgressPaused() - - component.presentController(actionSheet, nil) - }, - moreAction: { [weak self] sourceView, gesture in - guard let self else { - return - } - self.performMoreAction(sourceView: sourceView, gesture: gesture) - }, - openPeer: { [weak self] peer in - guard let self else { - return - } - self.navigateToPeer(peer: peer, chat: false) - } - )), - environment: {}, - containerSize: availableSize - ) - let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize) - if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { - var animateIn = false - if viewListView.superview == nil { - self.addSubview(viewListView) - animateIn = true - } - viewListTransition.setFrame(view: viewListView, frame: viewListFrame) - viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI || self.isEditingStory ? 0.0 : 1.0) + )), + environment: {}, + containerSize: availableSize + ) - if animateIn, !transition.animation.isImmediate { - viewListView.animateIn(transition: transition) + var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize) + let indexDistance = CGFloat(max(-1, min(1, itemIndex - currentIndex))) + viewListFrame.origin.x += indexDistance * availableSize.width + + if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { + var animateIn = false + if viewListView.superview == nil { + self.addSubview(viewListView) + animateIn = true + } else { + fixedAnimationOffset = viewListFrame.minX - viewListView.frame.minX + } + viewListTransition.setFrame(view: viewListView, frame: viewListFrame) + viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI || self.isEditingStory ? 0.0 : 1.0) + + if animateIn, !transition.animation.isImmediate { + viewListView.animateIn(transition: transition) + } + } + if id == component.slice.item.storyItem.id { + viewListInset = viewList.externalState.effectiveHeight + inputPanelBottomInset = viewListInset } } - viewListInset = viewList.externalState.effectiveHeight - inputPanelBottomInset = viewListInset - } else if let viewList = self.viewList { - self.viewList = nil - if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { - viewListView.animateOut(transition: transition, completion: { [weak viewListView] in - viewListView?.removeFromSuperview() - }) + + if fixedAnimationOffset == 0.0 { + for (id, viewList) in self.viewLists { + if let viewListView = viewList.view.view, !visibleViewListIds.contains(id), let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) { + let viewListSize = viewListView.bounds.size + var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize) + let indexDistance = CGFloat(max(-1, min(1, itemIndex - currentIndex))) + viewListFrame.origin.x += indexDistance * availableSize.width + + fixedAnimationOffset = viewListFrame.minX - viewListView.frame.minX + } + } } + + if fixedAnimationOffset != 0.0 { + for id in applyFixedAnimationOffsetIds { + if let viewListView = self.viewLists[id]?.view.view { + itemsTransition.animatePosition(view: viewListView, from: CGPoint(x: -fixedAnimationOffset, y: 0.0), to: CGPoint(), additive: true) + } + } + } + } + var removeViewListIds: [Int32] = [] + for (id, viewList) in self.viewLists { + if !validViewListIds.contains(id) { + removeViewListIds.append(id) + viewList.view.view?.removeFromSuperview() + } + } + for id in removeViewListIds { + self.viewLists.removeValue(forKey: id) } let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778)) @@ -2000,7 +2197,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .manual, shouldDismissOnTouch: { _, _ in + text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in return .dismiss(consume: true) } ) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index be2c33fd56..2dc6fe0b22 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -13,6 +13,7 @@ import TelegramStringFormatting import ShimmerEffect import StoryFooterPanelComponent import PeerListItemComponent +import AnimatedStickerComponent final class StoryItemSetViewListComponent: Component { final class AnimationHint { @@ -47,6 +48,7 @@ final class StoryItemSetViewListComponent: Component { let safeInsets: UIEdgeInsets let storyItem: EngineStoryItem let outerExpansionFraction: CGFloat + let outerExpansionDirection: Bool let close: () -> Void let expandViewStats: () -> Void let deleteAction: () -> Void @@ -63,6 +65,7 @@ final class StoryItemSetViewListComponent: Component { safeInsets: UIEdgeInsets, storyItem: EngineStoryItem, outerExpansionFraction: CGFloat, + outerExpansionDirection: Bool, close: @escaping () -> Void, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, @@ -78,6 +81,7 @@ final class StoryItemSetViewListComponent: Component { self.safeInsets = safeInsets self.storyItem = storyItem self.outerExpansionFraction = outerExpansionFraction + self.outerExpansionDirection = outerExpansionDirection self.close = close self.expandViewStats = expandViewStats self.deleteAction = deleteAction @@ -104,6 +108,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.outerExpansionFraction != rhs.outerExpansionFraction { return false } + if lhs.outerExpansionDirection != rhs.outerExpansionDirection { + return false + } return true } @@ -194,6 +201,9 @@ final class StoryItemSetViewListComponent: Component { private var visibleItems: [EnginePeer.Id: ComponentView] = [:] private var visiblePlaceholderViews: [Int: UIImageView] = [:] + + private var emptyIcon: ComponentView? + private var emptyText: ComponentView? private var component: StoryItemSetViewListComponent? private weak var state: EmptyComponentState? @@ -231,7 +241,9 @@ final class StoryItemSetViewListComponent: Component { self.addSubview(self.navigationBarBackground) self.layer.addSublayer(self.navigationSeparator) - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { _ in + return [.down] + }) panRecognizer.delegate = self self.addGestureRecognizer(panRecognizer) } @@ -245,7 +257,11 @@ final class StoryItemSetViewListComponent: Component { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true + if otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } else { + return false + } } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { @@ -321,7 +337,10 @@ final class StoryItemSetViewListComponent: Component { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let navigationPanelView = self.navigationPanel.view { + if let navigationPanelView = self.navigationPanel.view as? StoryFooterPanelComponent.View { + if navigationPanelView.frame.contains(point), let result = navigationPanelView.externalContainerView.hitTest(self.convert(point, to: navigationPanelView.externalContainerView), with: event), result !== navigationPanelView.externalContainerView { + return result + } if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) { if result !== navigationPanelView { return result @@ -455,7 +474,7 @@ final class StoryItemSetViewListComponent: Component { theme: component.theme, strings: component.strings, style: .generic, - sideInset: itemLayout.sideInset, + sideInset: 0.0, title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), peer: item.peer, subtitle: dateText, @@ -530,6 +549,7 @@ final class StoryItemSetViewListComponent: Component { func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme let itemUpdated = self.component?.storyItem.id != component.storyItem.id + let viewsNilUpdated = (self.component?.storyItem.views == nil) != (component.storyItem.views == nil) self.component = component self.state = state @@ -539,7 +559,7 @@ final class StoryItemSetViewListComponent: Component { synchronous = animationHint.synchronous } - let minimizedHeight = min(availableSize.height, 500.0) + let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0)) if themeUpdated { self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor @@ -547,7 +567,7 @@ final class StoryItemSetViewListComponent: Component { self.navigationSeparator.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor } - if itemUpdated { + if itemUpdated || viewsNilUpdated { self.viewListState = nil self.viewListDisposable?.dispose() @@ -606,7 +626,7 @@ final class StoryItemSetViewListComponent: Component { environment: {}, containerSize: CGSize(width: 120.0, height: 100.0) ) - let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY), size: navigationLeftButtonSize) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY + 1.0), size: navigationLeftButtonSize) if let navigationLeftButtonView = self.navigationLeftButton.view { if navigationLeftButtonView.superview == nil { self.addSubview(navigationLeftButtonView) @@ -621,7 +641,17 @@ final class StoryItemSetViewListComponent: Component { dismissOffsetY = -dismissPanState.accumulatedOffset } - dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset + let selfFraction = expansionOffset / availableSize.height + + var mappedOuterExpansionFraction: CGFloat + if component.outerExpansionDirection { + mappedOuterExpansionFraction = component.outerExpansionFraction / (1.0 - selfFraction) + } else { + mappedOuterExpansionFraction = 1.0 - (1.0 - component.outerExpansionFraction) / (1.0 - selfFraction) + } + mappedOuterExpansionFraction = max(0.0, min(1.0, mappedOuterExpansionFraction)) + + dismissOffsetY -= (1.0 - mappedOuterExpansionFraction) * expansionOffset let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset)) @@ -744,6 +774,82 @@ final class StoryItemSetViewListComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) + if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 { + var emptyTransition = transition + + let emptyIcon: ComponentView + if let current = self.emptyIcon { + emptyIcon = current + } else { + emptyTransition = emptyTransition.withAnimation(.none) + emptyIcon = ComponentView() + self.emptyIcon = emptyIcon + } + + let emptyText: ComponentView + if let current = self.emptyText { + emptyText = current + } else { + emptyText = ComponentView() + self.emptyText = emptyText + } + + let emptyIconSize = emptyIcon.update( + transition: emptyTransition, + component: AnyComponent(AnimatedStickerComponent( + account: component.context.account, + animation: AnimatedStickerComponent.Animation(source: .bundle(name: "ChatListNoResults"), loop: true), + size: CGSize(width: 140.0, height: 140.0) + )), + environment: {}, + containerSize: CGSize(width: 140.0, height: 140.0) + ) + + let text: String + if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { + text = "List of viewers isn’t available after\n24 hours of story expiration." + } else { + text = "Nobody has viewed\nyour story yet." + } + let textSize = emptyText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: text, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + + let emptyContentSpacing: CGFloat = 20.0 + var emptyContentY = navigationBarFrame.minY + floor((availableSize.height - navigationBarFrame.minY - (emptyIconSize.height - emptyContentSpacing - textSize.height)) * 0.5) - 60.0 + + if let emptyIconView = emptyIcon.view { + if emptyIconView.superview == nil { + self.insertSubview(emptyIconView, belowSubview: self.scrollView) + } + emptyTransition.setFrame(view: emptyIconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - emptyIconSize.width) * 0.5), y: emptyContentY), size: emptyIconSize)) + emptyContentY += emptyIconSize.height + emptyContentSpacing + } + + if let emptyTextView = emptyText.view { + if emptyTextView.superview == nil { + self.insertSubview(emptyTextView, belowSubview: self.scrollView) + } + emptyTransition.setFrame(view: emptyTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: emptyContentY), size: textSize)) + } + } else { + if let emptyIcon = self.emptyIcon { + self.emptyIcon = nil + emptyIcon.view?.removeFromSuperview() + } + if let emptyText = self.emptyText { + self.emptyText = nil + emptyText.view?.removeFromSuperview() + } + } + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY)) component.externalState.minimizedHeight = minimizedHeight diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index e1fe3fbbea..22edbb0781 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -277,7 +277,7 @@ public final class StoryFooterPanelComponent: Component { ) 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: 2.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize) + let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 3.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize) let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction) let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 7b0e2699bc..dfce3dbe0e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -721,7 +721,7 @@ public final class StoryPeerListComponent: Component { let centralContentWidth: CGFloat = collapsedContentWidth + titleContentSpacing + collapsedState.titleWidth - collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5) + collapsedContentOrigin = (itemLayout.containerSize.width - centralContentWidth) * 0.5 collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0) @@ -1135,7 +1135,7 @@ public final class StoryPeerListComponent: Component { let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction * (1.0 - collapsedState.activityFraction)) - var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction * (1.0 - collapsedState.activityFraction)) + var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: ((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction * (1.0 - collapsedState.activityFraction)) var titleIndicatorSize: CGSize? if collapsedState.activityFraction != 0.0 { diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift index f4e01a6351..767f2cf56b 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -49,7 +49,7 @@ private final class ShapeImageView: UIView { context.setBlendMode(.sourceIn) let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map { - UIColor(rgb: $0).cgColor + UIColor(argb: $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: []) @@ -344,25 +344,25 @@ public final class StorySetIndicatorComponent: Component { if component.theme.overallDarkAppearance { if component.hasUnseen { borderColors = [ - 0x34C76F, - 0x3DA1FD + 0xFF34C76F, + 0xFF3DA1FD ] } else { borderColors = [ - 0x48484A, - 0x48484A + UIColor(white: 1.0, alpha: 0.3).argb, + UIColor(white: 1.0, alpha: 0.3).argb ] } } else { if component.hasUnseen { borderColors = [ - 0x34C76F, - 0x3DA1FD + 0xFF34C76F, + 0xFF3DA1FD ] } else { borderColors = [ - 0xD8D8E1, - 0xD8D8E1 + UIColor(white: 1.0, alpha: 0.3).argb, + UIColor(white: 1.0, alpha: 0.3).argb ] } }