From de8c3f055f4b6d74716af39cf0feeedb121e566c Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 01:07:41 +0400 Subject: [PATCH 01/13] [WIP] Stories --- .../Sources/ChatListController.swift | 1 + .../Sources/ContactsController.swift | 3 +- .../Sources/GridMessageSelectionNode.swift | 2 +- .../Sources/MediaTrackFrameBuffer.swift | 2 +- .../Sources/SparseItemGrid.swift | 159 ++++--- .../Sources/ManagedAudioSession.swift | 6 - .../TelegramEngine/Messages/Stories.swift | 156 +++++-- .../Messages/StoryListContext.swift | 80 +++- .../Messages/TelegramEngineMessages.swift | 12 +- .../PresentationResourcesSettings.swift | 4 +- .../BottomButtonPanelComponent/BUILD | 23 + .../Sources/BottomButtonPanelComponent.swift} | 28 +- .../ChatTitleView/Sources/ChatTitleView.swift | 4 +- .../PeerInfo/PeerInfoStoryGridScreen/BUILD | 3 + .../Sources/PeerInfoStoryGridScreen.swift | 428 +++++++++++++----- .../Sources/PeerInfoStoryPaneNode.swift | 267 ++++++++--- .../Sources/PeerInfoVisualMediaPaneNode.swift | 2 +- .../Components/StorageUsageScreen/BUILD | 1 + .../Sources/StorageUsageScreen.swift | 3 +- .../Sources/StoryContainerScreen.swift | 119 ++++- .../Sources/StoryContent.swift | 19 +- .../StoryItemSetContainerComponent.swift | 87 +++- .../Sources/StoryChatContent.swift | 144 ++++-- .../Sources/StoryItemContentComponent.swift | 5 +- .../Menu/Stories.imageset/Contents.json | 12 + .../Menu/Stories.imageset/Stories.svg | 5 + .../TelegramUI/Sources/ChatController.swift | 1 + .../PeerInfo/PeerInfoPaneContainerNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- 29 files changed, 1171 insertions(+), 409 deletions(-) create mode 100644 submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD rename submodules/TelegramUI/Components/{StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift => BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift} (84%) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2a547c3f71..452b249cbd 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2362,6 +2362,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryContainerScreen.TransitionOut( destinationView: transitionView, + transitionView: nil, destinationRect: transitionView.bounds, destinationCornerRadius: transitionView.bounds.height * 0.5, destinationIsAvatar: true, diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 10329ebdae..b4f9bf94be 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -515,7 +515,7 @@ public class ContactsController: ViewController { return } - let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peer?.id) + let storyContent = StoryContentContextImpl(context: self.context, includeHidden: true, focusedPeerId: peer?.id) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] storyContentState in @@ -551,6 +551,7 @@ public class ContactsController: ViewController { if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryContainerScreen.TransitionOut( destinationView: transitionView, + transitionView: nil, destinationRect: transitionView.bounds, destinationCornerRadius: transitionView.bounds.height * 0.5, destinationIsAvatar: true, diff --git a/submodules/GridMessageSelectionNode/Sources/GridMessageSelectionNode.swift b/submodules/GridMessageSelectionNode/Sources/GridMessageSelectionNode.swift index 98507d97e5..ee5776c341 100644 --- a/submodules/GridMessageSelectionNode/Sources/GridMessageSelectionNode.swift +++ b/submodules/GridMessageSelectionNode/Sources/GridMessageSelectionNode.swift @@ -62,7 +62,7 @@ public final class GridMessageSelectionNode: ASDisplayNode { public final class GridMessageSelectionLayer: CALayer { private var selected = false - private let checkLayer: CheckLayer + public let checkLayer: CheckLayer public init(theme: CheckNodeTheme) { self.checkLayer = CheckLayer(theme: theme, content: .check) diff --git a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift index 430e17dd8f..de302f827a 100644 --- a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift +++ b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift @@ -17,7 +17,7 @@ public enum MediaTrackFrameResult { } private let traceEvents: Bool = { - #if DEBUG + #if DEBUG && false return true #else return false diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 883e7921fe..2c73a1e66a 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -35,7 +35,7 @@ public protocol SparseItemGridBinding: AnyObject { func unbindLayer(layer: SparseItemGridLayer) func scrollerTextForTag(tag: Int32) -> String? func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal - func onTap(item: SparseItemGrid.Item) + func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) func onTagTap() func didScroll() func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) @@ -667,6 +667,27 @@ public final class SparseItemGrid: ASDisplayNode { return nil } + + func itemHitTest(at point: CGPoint) -> (Item, CALayer, CGPoint)? { + guard let items = self.items, !items.items.isEmpty else { + return nil + } + + let localPoint = self.scrollView.convert(point, from: self.view) + + for (id, visibleItem) in self.visibleItems { + if visibleItem.frame.contains(localPoint) { + for item in items.items { + if item.id == id { + return (item, visibleItem.displayLayer, self.view.layer.convert(point, to: visibleItem.displayLayer)) + } + } + return nil + } + } + + return nil + } func anchorItem(at point: CGPoint, orLower: Bool = false) -> (Item, Int)? { guard let items = self.items, !items.items.isEmpty, let layout = self.layout else { @@ -862,7 +883,12 @@ public final class SparseItemGrid: ASDisplayNode { return } - let contentHeight = layout.contentHeight(count: items.count) + let contentHeight: CGFloat + if items.items.isEmpty { + contentHeight = 0.0 + } else { + contentHeight = layout.contentHeight(count: items.count) + } let shimmerColors = items.itemBinding.getShimmerColors() if resetScrolling { @@ -904,83 +930,82 @@ public final class SparseItemGrid: ASDisplayNode { var validIds = Set() var usedPlaceholderCount = 0 - if !items.items.isEmpty { - var bindItems: [Item] = [] - var bindLayers: [SparseItemGridDisplayItem] = [] - var updateLayers: [SparseItemGridDisplayItem] = [] + + var bindItems: [Item] = [] + var bindLayers: [SparseItemGridDisplayItem] = [] + var updateLayers: [SparseItemGridDisplayItem] = [] - let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) - for index in visibleRange.minIndex ... visibleRange.maxIndex { - if let item = items.item(at: index) { - let itemFrame = layout.frame(at: index) + let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) + for index in visibleRange.minIndex ... visibleRange.maxIndex { + if let item = items.item(at: index) { + let itemFrame = layout.frame(at: index) - let itemLayer: VisibleItem - if let current = self.visibleItems[item.id] { - itemLayer = current - updateLayers.append(itemLayer) - } else { - itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView()) - self.visibleItems[item.id] = itemLayer - - bindItems.append(item) - bindLayers.append(itemLayer) - - if let layer = itemLayer.layer { - self.scrollView.layer.addSublayer(layer) - } else if let view = itemLayer.view { - self.scrollView.addSubview(view) - } - } - - if itemLayer.needsShimmer { - let placeholderLayer: SparseItemGridShimmerLayer - if let current = itemLayer.shimmerLayer { - placeholderLayer = current - } else { - placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer() - self.scrollView.layer.insertSublayer(placeholderLayer, at: 0) - itemLayer.shimmerLayer = placeholderLayer - } - - placeholderLayer.frame = itemFrame - self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) - placeholderLayer.update(size: itemFrame.size) - } else if let placeholderLayer = itemLayer.shimmerLayer { - itemLayer.shimmerLayer = nil - placeholderLayer.removeFromSuperlayer() - } - - validIds.insert(item.id) - - itemLayer.frame = itemFrame + let itemLayer: VisibleItem + if let current = self.visibleItems[item.id] { + itemLayer = current + updateLayers.append(itemLayer) } else { + itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView()) + self.visibleItems[item.id] = itemLayer + + bindItems.append(item) + bindLayers.append(itemLayer) + + if let layer = itemLayer.layer { + self.scrollView.layer.addSublayer(layer) + } else if let view = itemLayer.view { + self.scrollView.addSubview(view) + } + } + + if itemLayer.needsShimmer { let placeholderLayer: SparseItemGridShimmerLayer - if self.visiblePlaceholders.count > usedPlaceholderCount { - placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount] + if let current = itemLayer.shimmerLayer { + placeholderLayer = current } else { placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer() - self.scrollView.layer.addSublayer(placeholderLayer) - self.visiblePlaceholders.append(placeholderLayer) + self.scrollView.layer.insertSublayer(placeholderLayer, at: 0) + itemLayer.shimmerLayer = placeholderLayer } - let itemFrame = layout.frame(at: index) + placeholderLayer.frame = itemFrame self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) placeholderLayer.update(size: itemFrame.size) - usedPlaceholderCount += 1 + } else if let placeholderLayer = itemLayer.shimmerLayer { + itemLayer.shimmerLayer = nil + placeholderLayer.removeFromSuperlayer() } - } - if !bindItems.isEmpty { - items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous) - } + validIds.insert(item.id) - for item in updateLayers { - let item = item as! VisibleItem - if let layer = item.layer { - layer.update(size: layer.frame.size) - } else if let view = item.view { - view.update(size: layer.frame.size, insets: layout.containerLayout.insets) + itemLayer.frame = itemFrame + } else { + let placeholderLayer: SparseItemGridShimmerLayer + if self.visiblePlaceholders.count > usedPlaceholderCount { + placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount] + } else { + placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer() + self.scrollView.layer.addSublayer(placeholderLayer) + self.visiblePlaceholders.append(placeholderLayer) } + let itemFrame = layout.frame(at: index) + placeholderLayer.frame = itemFrame + self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) + placeholderLayer.update(size: itemFrame.size) + usedPlaceholderCount += 1 + } + } + + if !bindItems.isEmpty { + items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous) + } + + for item in updateLayers { + let item = item as! VisibleItem + if let layer = item.layer { + layer.update(size: layer.frame.size) + } else if let view = item.view { + view.update(size: layer.frame.size, insets: layout.containerLayout.insets) } } @@ -1398,8 +1423,8 @@ public final class SparseItemGrid: ASDisplayNode { } if case .ended = recognizer.state { let location = recognizer.location(in: self.view) - if let item = currentViewport.item(at: self.view.convert(location, to: currentViewport.view)) { - items.itemBinding.onTap(item: item) + if let (item, itemLayer, point) = currentViewport.itemHitTest(at: self.view.convert(location, to: currentViewport.view)) { + items.itemBinding.onTap(item: item, itemLayer: itemLayer, point: point) } } } diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 63055b6364..83c6ec8e9c 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -780,12 +780,6 @@ public final class ManagedAudioSession { managedAudioSessionLog("ManagedAudioSession resetting options") try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options) } - /*if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: mode, policy: .default, options: options) - } else { - AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: nativeCategory) - try AVAudioSession.sharedInstance().setMode(mode) - }*/ } catch let error { managedAudioSessionLog("ManagedAudioSession setup error \(error)") } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 634f7d928d..277a9e09d0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -810,16 +810,25 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In } } -func _internal_deleteStory(account: Account, id: Int32) -> Signal { +func _internal_deleteStories(account: Account, ids: [Int32]) -> Signal { return account.postbox.transaction { transaction -> Void in var items = transaction.getStoryItems(peerId: account.peerId) - if let index = items.firstIndex(where: { $0.id == id }) { - items.remove(at: index) + var updated = false + for id in ids { + if let index = items.firstIndex(where: { $0.id == id }) { + items.remove(at: index) + updated = true + } + } + if updated { transaction.setStoryItems(peerId: account.peerId, items: items) } + account.stateManager.injectStoryUpdates(updates: ids.map { id in + return .deleted(peerId: account.peerId, id: id) + }) } |> mapToSignal { _ -> Signal in - return account.network.request(Api.functions.stories.deleteStories(id: [id])) + return account.network.request(Api.functions.stories.deleteStories(id: ids)) |> `catch` { _ -> Signal<[Int32], NoError> in return .single([]) } @@ -829,67 +838,114 @@ func _internal_deleteStory(account: Account, id: Int32) -> Signal Signal { - return account.postbox.transaction { transaction -> Api.InputUser? in - if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) { - transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( - subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState, - maxReadId: max(peerStoryState.maxReadId, id) - ))) +func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPinned: Bool) -> Signal { + if asPinned { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(peerId).flatMap(apiInputUser) } - - return transaction.getPeer(peerId).flatMap(apiInputUser) - } - |> mapToSignal { inputUser -> Signal in - guard let inputUser = inputUser else { - return .complete() + |> mapToSignal { inputUser -> Signal in + guard let inputUser = inputUser else { + return .complete() + } + + #if DEBUG && false + if "".isEmpty { + return .complete() + } + #endif + + return account.network.request(Api.functions.stories.incrementStoryViews(userId: inputUser, id: [id])) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues } - - account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)]) - - #if DEBUG - if "".isEmpty { - return .complete() + } else { + return account.postbox.transaction { transaction -> Api.InputUser? in + if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) { + transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( + subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState, + maxReadId: max(peerStoryState.maxReadId, id) + ))) + } + + return transaction.getPeer(peerId).flatMap(apiInputUser) } - #endif - - return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id)) - |> `catch` { _ -> Signal<[Int32], NoError> in - return .single([]) + |> mapToSignal { inputUser -> Signal in + guard let inputUser = inputUser else { + return .complete() + } + + account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)]) + + #if DEBUG && false + if "".isEmpty { + return .complete() + } + #endif + + return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id)) + |> `catch` { _ -> Signal<[Int32], NoError> in + return .single([]) + } + |> ignoreValues } - |> ignoreValues } } -func _internal_updateStoryIsPinned(account: Account, id: Int32, isPinned: Bool) -> Signal { +func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal { return account.postbox.transaction { transaction -> Void in var items = transaction.getStoryItems(peerId: account.peerId) - if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) { - let updatedItem = Stories.Item( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: item.media, - text: item.text, - entities: item.entities, - views: item.views, - privacy: item.privacy, - isPinned: isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic - ) - if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { - items[index] = StoryItemsTableEntry(value: entry, id: item.id) - transaction.setStoryItems(peerId: account.peerId, items: items) + var updatedItems: [Stories.Item] = [] + for (id, referenceItem) in ids { + if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) { + let updatedItem = Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { + items[index] = StoryItemsTableEntry(value: entry, id: item.id) + } + + updatedItems.append(updatedItem) + } else { + let item = referenceItem.asStoryItem() + let updatedItem = Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + updatedItems.append(updatedItem) } - + } + transaction.setStoryItems(peerId: account.peerId, items: items) + if !updatedItems.isEmpty { DispatchQueue.main.async { - account.stateManager.injectStoryUpdates(updates: [.added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))]) + account.stateManager.injectStoryUpdates(updates: updatedItems.map { updatedItem in + return .added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem)) + }) } } } |> mapToSignal { _ -> Signal in - return account.network.request(Api.functions.stories.togglePinned(id: [id], pinned: isPinned ? .boolTrue : .boolFalse)) + return account.network.request(Api.functions.stories.togglePinned(id: ids.keys.sorted(), pinned: isPinned ? .boolTrue : .boolFalse)) |> `catch` { _ -> Signal<[Int32], NoError> in return .single([]) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index e9aee99686..e88a71a258 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -95,6 +95,34 @@ public final class EngineStoryItem: Equatable { } } +extension EngineStoryItem { + func asStoryItem() -> Stories.Item { + return Stories.Item( + id: self.id, + timestamp: self.timestamp, + expirationTimestamp: self.expirationTimestamp, + media: self.media._asMedia(), + text: self.text, + entities: self.entities, + views: self.views.flatMap { views in + return Stories.Item.Views( + seenCount: views.seenCount, + seenPeerIds: views.seenPeers.map(\.id) + ) + }, + privacy: self.privacy.flatMap { privacy in + return Stories.Item.Privacy( + base: privacy.base, + additionallyIncludePeers: privacy.additionallyIncludePeers + ) + }, + isPinned: self.isPinned, + isExpired: self.isExpired, + isPublic: self.isPublic + ) + } +} + public final class StorySubscriptionsContext { private enum OpaqueStateMark: Equatable { case empty @@ -599,15 +627,17 @@ public final class PeerStoryListContext { return } + var finalUpdatedState: State? + for update in updates { switch update { case let .deleted(peerId, id): if self.peerId == peerId { if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) { - var updatedState = self.stateValue + var updatedState = finalUpdatedState ?? self.stateValue updatedState.items.remove(at: index) updatedState.totalCount = max(0, updatedState.totalCount - 1) - self.stateValue = updatedState + finalUpdatedState = updatedState } } case let .added(peerId, item): @@ -617,7 +647,7 @@ public final class PeerStoryListContext { if case let .item(item) = item { if item.isPinned { if let media = item.media { - var updatedState = self.stateValue + var updatedState = finalUpdatedState ?? self.stateValue updatedState.items[index] = EngineStoryItem( id: item.id, timestamp: item.timestamp, @@ -638,13 +668,47 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic ) - self.stateValue = updatedState + finalUpdatedState = updatedState } } else { - var updatedState = self.stateValue + var updatedState = finalUpdatedState ?? self.stateValue updatedState.items.remove(at: index) updatedState.totalCount = max(0, updatedState.totalCount - 1) - self.stateValue = updatedState + finalUpdatedState = updatedState + } + } + } + } else { + if !self.isArchived { + if case let .item(item) = item { + if item.isPinned { + if let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.append(EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + )) + updatedState.items.sort(by: { lhs, rhs in + return lhs.timestamp > rhs.timestamp + }) + finalUpdatedState = updatedState + } } } } @@ -654,6 +718,10 @@ public final class PeerStoryListContext { break } } + + if let finalUpdatedState = finalUpdatedState { + self.stateValue = finalUpdatedState + } }) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 8754438ac8..805b7d9fc0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -926,16 +926,16 @@ public extension TelegramEngine { return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy) } - public func deleteStory(id: Int32) -> Signal { - return _internal_deleteStory(account: self.account, id: id) + public func deleteStories(ids: [Int32]) -> Signal { + return _internal_deleteStories(account: self.account, ids: ids) } - public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32) -> Signal { - return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id) + public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal { + return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned) } - public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal { - return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned) + public func updateStoriesArePinned(ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal { + return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned) } public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 225ae4a1d9..c122f261ae 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -29,16 +29,14 @@ public struct PresentationResourcesSettings { public static let devices = renderIcon(name: "Settings/Menu/Sessions") public static let chatFolders = renderIcon(name: "Settings/Menu/ChatListFilters") public static let stickers = renderIcon(name: "Settings/Menu/Stickers") - public static let notifications = renderIcon(name: "Settings/Menu/Notifications") public static let security = renderIcon(name: "Settings/Menu/Security") public static let dataAndStorage = renderIcon(name: "Settings/Menu/DataAndStorage") public static let appearance = renderIcon(name: "Settings/Menu/Appearance") public static let language = renderIcon(name: "Settings/Menu/Language") - public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon") - public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving") + public static let stories = renderIcon(name: "Settings/Menu/Stories") public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD b/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD new file mode 100644 index 0000000000..ebd3662a5c --- /dev/null +++ b/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BottomButtonPanelComponent", + module_name = "BottomButtonPanelComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/SolidRoundedButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift similarity index 84% rename from submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift rename to submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift index 96a324bb12..004611ea4d 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift +++ b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift @@ -3,19 +3,11 @@ import UIKit import Display import AsyncDisplayKit import ComponentFlow -import SwiftSignalKit -import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData -import AccountContext -import TelegramCore -import MultilineTextComponent -import EmojiStatusComponent -import TelegramStringFormatting -import CheckNode import SolidRoundedButtonComponent -final class StorageUsageScreenSelectionPanelComponent: Component { +public final class BottomButtonPanelComponent: Component { let theme: PresentationTheme let title: String let label: String? @@ -23,7 +15,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component { let insets: UIEdgeInsets let action: () -> Void - init( + public init( theme: PresentationTheme, title: String, label: String?, @@ -39,7 +31,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component { self.action = action } - static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool { + public static func ==(lhs: BottomButtonPanelComponent, rhs: BottomButtonPanelComponent) -> Bool { if lhs.theme !== rhs.theme { return false } @@ -58,14 +50,14 @@ final class StorageUsageScreenSelectionPanelComponent: Component { return true } - class View: UIView { + public class View: UIView { private let backgroundView: BlurredBackgroundView private let separatorLayer: SimpleLayer private let actionButton = ComponentView() - private var component: StorageUsageScreenSelectionPanelComponent? + private var component: BottomButtonPanelComponent? - override init(frame: CGRect) { + override public init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.separatorLayer = SimpleLayer() @@ -75,11 +67,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component { self.layer.addSublayer(self.separatorLayer) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -146,11 +138,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + 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/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index ea24a0aad3..5346f17a39 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -117,6 +117,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { private let button: HighlightTrackingButtonNode + public var disableAnimations: Bool = false + var manualLayout: Bool = false private var validLayout: (CGSize, CGRect)? @@ -356,7 +358,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if !self.updateStatus() { if updated { if !self.manualLayout, let (size, clearBounds) = self.validLayout { - let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut)) + let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: self.disableAnimations ? .immediate : .animated(duration: 0.2, curve: .easeInOut)) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index c566a59403..0a656a777f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -21,7 +21,10 @@ swift_library( "//submodules/Components/ViewControllerComponent", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/TelegramUI/Components/ChatTitleView", "//submodules/ContextUI", + "//submodules/UndoUI", + "//submodules/TelegramUI/Components/BottomButtonPanelComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index e35aad6ab6..be572d7296 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -10,6 +10,9 @@ import PeerInfoVisualMediaPaneNode import ViewControllerComponent import ChatListHeaderComponent import ContextUI +import ChatTitleView +import BottomButtonPanelComponent +import UndoUI final class PeerInfoStoryGridScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -48,6 +51,13 @@ final class PeerInfoStoryGridScreenComponent: Component { private var environment: EnvironmentType? private var paneNode: PeerInfoStoryPaneNode? + private var paneStatusDisposable: Disposable? + private(set) var paneStatusText: String? + + private(set) var selectedCount: Int = 0 + private var selectionStateDisposable: Disposable? + + private var selectionPanel: ComponentView? private weak var mediaGalleryContextMenu: ContextController? @@ -59,6 +69,11 @@ final class PeerInfoStoryGridScreenComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.paneStatusDisposable?.dispose() + self.selectionStateDisposable?.dispose() + } + func morePressed(source: ContextReferenceContentNode) { guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else { return @@ -68,120 +83,168 @@ final class PeerInfoStoryGridScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - - var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? - let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in - let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement - let canZoom: Bool = nextZoomLevel != nil - - return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) - }, action: canZoom ? { action in - guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { - return - } - pane.updateZoomLevel(level: zoomLevel) - if let recurseGenerateAction = recurseGenerateAction { - action.updateAction(0, recurseGenerateAction(true)) - action.updateAction(1, recurseGenerateAction(false)) - } - } : nil) - } - recurseGenerateAction = { isZoomIn in - return generateAction(isZoomIn) - } - - items.append(.action(generateAction(true))) - items.append(.action(generateAction(false))) - if component.peerId == component.context.account.peerId, case .saved = component.scope { - var ignoreNextActions = false + if self.selectedCount != 0 { //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor) + //TODO:update icon + items.append(.action(ContextMenuActionItem(text: "Save to Photos", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in - if ignoreNextActions { - return - } - ignoreNextActions = true a(.default) guard let self, let component = self.component else { return } - self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive)) + let _ = component }))) + items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else { + return + } + let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start() + + //TODO:localize + let text: String + if paneNode.selectedIds.count == 1 { + text = "1 story deleted." + } else { + text = "\(paneNode.selectedIds.count) stories deleted." + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: text, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + + paneNode.clearSelection() + }))) + } else { + var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? + let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in + let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement + let canZoom: Bool = nextZoomLevel != nil + + return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) + }, action: canZoom ? { action in + guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { + return + } + pane.updateZoomLevel(level: zoomLevel) + if let recurseGenerateAction = recurseGenerateAction { + action.updateAction(0, recurseGenerateAction(true)) + action.updateAction(1, recurseGenerateAction(false)) + } + } : nil) + } + recurseGenerateAction = { isZoomIn in + return generateAction(isZoomIn) + } + + items.append(.action(generateAction(true))) + items.append(.action(generateAction(false))) + + if component.peerId == component.context.account.peerId, case .saved = component.scope { + var ignoreNextActions = false + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + guard let self, let component = self.component else { + return + } + + self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive)) + }))) + } + + /*if photoCount != 0 && videoCount != 0 { + items.append(.separator) + + let showPhotos: Bool + switch pane.contentType { + case .photo, .photoOrVideo: + showPhotos = true + default: + showPhotos = false + } + let showVideos: Bool + switch pane.contentType { + case .video, .photoOrVideo: + showVideos = true + default: + showVideos = false + } + + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in + if !showPhotos { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + a(.default) + + guard let pane = pane else { + return + } + let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType + switch pane.contentType { + case .photoOrVideo: + updatedContentType = .video + case .photo: + updatedContentType = .photo + case .video: + updatedContentType = .photoOrVideo + default: + updatedContentType = pane.contentType + } + pane.updateContentType(contentType: updatedContentType) + }))) + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in + if !showVideos { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + a(.default) + + guard let pane = pane else { + return + } + let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType + switch pane.contentType { + case .photoOrVideo: + updatedContentType = .photo + case .photo: + updatedContentType = .photoOrVideo + case .video: + updatedContentType = .video + default: + updatedContentType = pane.contentType + } + pane.updateContentType(contentType: updatedContentType) + }))) + }*/ } - /*if photoCount != 0 && videoCount != 0 { - items.append(.separator) - - let showPhotos: Bool - switch pane.contentType { - case .photo, .photoOrVideo: - showPhotos = true - default: - showPhotos = false - } - let showVideos: Bool - switch pane.contentType { - case .video, .photoOrVideo: - showVideos = true - default: - showVideos = false - } - - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in - if !showPhotos { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak pane] _, a in - a(.default) - - guard let pane = pane else { - return - } - let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType - switch pane.contentType { - case .photoOrVideo: - updatedContentType = .video - case .photo: - updatedContentType = .photo - case .video: - updatedContentType = .photoOrVideo - default: - updatedContentType = pane.contentType - } - pane.updateContentType(contentType: updatedContentType) - }))) - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in - if !showVideos { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak pane] _, a in - a(.default) - - guard let pane = pane else { - return - } - let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType - switch pane.contentType { - case .photoOrVideo: - updatedContentType = .photo - case .photo: - updatedContentType = .photoOrVideo - case .video: - updatedContentType = .video - default: - updatedContentType = pane.contentType - } - pane.updateContentType(contentType: updatedContentType) - }))) - }*/ - let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) contextController.passthroughTouchEvent = { [weak self] sourceView, point in guard let self else { @@ -217,6 +280,8 @@ final class PeerInfoStoryGridScreenComponent: Component { self.component = component self.state = state + let sideInset: CGFloat = 14.0 + let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -227,6 +292,82 @@ final class PeerInfoStoryGridScreenComponent: Component { self.backgroundColor = environment.theme.list.plainBackgroundColor } + var bottomInset: CGFloat = environment.safeInsets.bottom + + if self.selectedCount != 0 { + let selectionPanel: ComponentView + var selectionPanelTransition = transition + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = .immediate + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + //TODO:localize + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(BottomButtonPanelComponent( + theme: environment.theme, + title: "Save to Profile", + label: nil, + isEnabled: true, + insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset), + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else { + return + } + + let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start() + + //TODO:localize + let title: String + if paneNode.selectedIds.count == 1 { + title = "Story saved to your profile" + } else { + title = "\(paneNode.selectedIds.count) saved to your profile" + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: title, 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 } + ), in: .current) + + paneNode.clearSelection() + } + )), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view { + var animateIn = false + if selectionPanelView.superview == nil { + self.addSubview(selectionPanelView) + animateIn = true + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize)) + if animateIn { + transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true) + } + } + bottomInset = selectionPanelSize.height + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view { + transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + let paneNode: PeerInfoStoryPaneNode if let current = self.paneNode { paneNode = current @@ -237,6 +378,7 @@ final class PeerInfoStoryGridScreenComponent: Component { chatLocation: .peer(id: component.peerId), contentType: .photoOrVideo, captureProtected: false, + isSaved: true, isArchive: component.scope == .archive, navigationController: { [weak self] in guard let self else { @@ -247,13 +389,41 @@ final class PeerInfoStoryGridScreenComponent: Component { ) self.paneNode = paneNode self.addSubview(paneNode.view) + + self.paneStatusDisposable = (paneNode.status + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + if self.paneStatusText != status?.text { + self.paneStatusText = status?.text + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + } + }) + + var applyState = false + self.selectionStateDisposable = (paneNode.updatedSelectedIds + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] selectedIds in + guard let self else { + return + } + + self.selectedCount = selectedIds.count + + if applyState { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + }) + applyState = true } paneNode.update( size: availableSize, topInset: environment.navigationHeight, sideInset: environment.safeInsets.left, - bottomInset: environment.safeInsets.bottom, + bottomInset: bottomInset, visibleHeight: availableSize.height, isScrollingLockedAtTop: false, expandProgress: 1.0, @@ -283,8 +453,11 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { } private let context: AccountContext + private let scope: Scope private var isDismissed: Bool = false + private var titleView: ChatTitleView? + private var moreBarButton: MoreHeaderButton? private var moreBarButtonItem: UIBarButtonItem? @@ -294,6 +467,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { scope: Scope ) { self.context = context + self.scope = scope super.init(context: context, component: PeerInfoStoryGridScreenComponent( context: context, @@ -301,9 +475,6 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { scope: scope ), navigationBarAppearance: .default, theme: .default) - //TODO:localize - self.navigationItem.title = "My Stories" - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor) moreBarButton.isUserInteractionEnabled = true @@ -321,6 +492,21 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside) self.navigationItem.setRightBarButton(moreBarButtonItem, animated: false) + + self.titleView = ChatTitleView( + context: context, theme: + presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer + ) + self.titleView?.disableAnimations = true + + self.navigationItem.titleView = self.titleView + + self.updateTitle() } required public init(coder aDecoder: NSCoder) { @@ -330,6 +516,34 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { deinit { } + func updateTitle() { + //TODO:localize + switch self.scope { + case .saved: + guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { + return + } + let title: String? + if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty { + title = paneStatusText + } else { + title = nil + } + self.titleView?.titleContent = .custom("My Stories", title, false) + case .archive: + guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { + return + } + let title: String + if componentView.selectedCount != 0 { + title = "\(componentView.selectedCount) Selected" + } else { + title = "Stories Archive" + } + self.titleView?.titleContent = .custom(title, nil, false) + } + } + @objc private func morePressed() { guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { return @@ -342,6 +556,8 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + + self.titleView?.layout = layout } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index dd3118df18..f74332837a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -476,6 +476,49 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } } +private final class ItemTransitionView: UIView { + private weak var itemLayer: ItemLayer? + private var copyDurationLayer: SimpleLayer? + + private var durationLayerBottomLeftPosition: CGPoint? + + init(itemLayer: ItemLayer?) { + self.itemLayer = itemLayer + + super.init(frame: CGRect()) + + if let itemLayer { + self.layer.contents = itemLayer.contents + self.layer.contentsRect = itemLayer.contentsRect + + if let durationLayer = itemLayer.durationLayer { + let copyDurationLayer = SimpleLayer() + copyDurationLayer.contents = durationLayer.contents + copyDurationLayer.contentsRect = durationLayer.contentsRect + copyDurationLayer.contentsGravity = durationLayer.contentsGravity + copyDurationLayer.contentsScale = durationLayer.contentsScale + copyDurationLayer.frame = durationLayer.frame + self.layer.addSublayer(copyDurationLayer) + self.copyDurationLayer = copyDurationLayer + + self.durationLayerBottomLeftPosition = CGPoint(x: itemLayer.bounds.width - durationLayer.frame.maxX, y: itemLayer.bounds.height - durationLayer.frame.maxY) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(state: StoryContainerScreen.TransitionState, transition: Transition) { + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + + if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition { + transition.setFrame(layer: copyDurationLayer, frame: CGRect(origin: CGPoint(x: size.width - durationLayerBottomLeftPosition.x - copyDurationLayer.bounds.width, y: size.height - durationLayerBottomLeftPosition.y - copyDurationLayer.bounds.height), size: copyDurationLayer.bounds.size)) + } + } +} + private final class SparseItemGridBindingImpl: SparseItemGridBinding { let context: AccountContext let chatLocation: ChatLocation @@ -485,8 +528,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { var chatPresentationData: ChatPresentationData var checkNodeTheme: CheckNodeTheme + var itemInteraction: VisualMediaItemInteraction? var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal)? - var onTapImpl: ((VisualMediaItem) -> Void)? + var onTapImpl: ((VisualMediaItem, CALayer, CGPoint) -> Void)? var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? @@ -671,8 +715,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0)) } - //TODO:selection - layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false) + var isSelected: Bool? + if let selectedIds = self.itemInteraction?.selectedIds { + isSelected = selectedIds.contains(story.id) + } + layer.updateSelection(theme: self.checkNodeTheme, isSelected: isSelected, animated: false) layer.bind(item: item) } @@ -698,11 +745,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } - func onTap(item: SparseItemGrid.Item) { + func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { return } - self.onTapImpl?(item) + self.onTapImpl?(item, itemLayer, point) } func onTagTap() { @@ -756,6 +803,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private let context: AccountContext private let peerId: PeerId private let chatLocation: ChatLocation + private let isSaved: Bool private let isArchive: Bool public private(set) var contentType: ContentType private var contentTypePromise: ValuePromise @@ -778,6 +826,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return self._itemInteraction! } + public var selectedIds: Set { + return self.itemInteraction.selectedIds ?? Set() + } + private let selectedIdsPromise = ValuePromise>(Set()) + public var updatedSelectedIds: Signal, NoError> { + return self.selectedIdsPromise.get() + } + + public var selectedItems: [Int32: EngineStoryItem] { + var result: [Int32: EngineStoryItem] = [:] + for id in self.selectedIds { + if let items = self.items { + for item in items.items { + if let item = item as? VisualMediaItem { + if item.story.id == id { + result[id] = item.story + } + } + } + } + } + return result + } + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() @@ -818,13 +890,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) { + public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) { self.context = context self.peerId = peerId self.chatLocation = chatLocation self.contentType = contentType self.contentTypePromise = ValuePromise(contentType) self.navigationController = navigationController + self.isSaved = isSaved self.isArchive = isArchive self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -867,10 +940,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return strongSelf.loadHole(anchor: hole, at: location) } - self.itemGridBinding.onTapImpl = { [weak self] item in + self.itemGridBinding.onTapImpl = { [weak self] item, itemLayer, point in guard let self else { return } + + if let selectedIds = self.itemInteraction.selectedIds, let itemLayer = itemLayer as? ItemLayer, let selectionLayer = itemLayer.selectionLayer { + if selectionLayer.checkLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id)) + return + } + } + //TODO:selection let listContext = PeerStoryListContentContextImpl( context: self.context, @@ -928,6 +1009,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer) return StoryContainerScreen.TransitionOut( destinationView: self.view, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak foundItemLayer] in + return ItemTransitionView(itemLayer: foundItemLayer as? ItemLayer) + }, + updateView: { view, state, transition in + (view as? ItemTransitionView)?.update(state: state, transition: transition) + } + ), destinationRect: self.itemGrid.view.convert(itemRect, to: self.view), destinationCornerRadius: 0.0, destinationIsAvatar: false, @@ -938,6 +1027,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return nil } ) + + self.hiddenMediaDisposable?.dispose() + self.hiddenMediaDisposable = (storyContainerScreen.focusedItem + |> deliverOnMainQueue).start(next: { [weak self] itemId in + guard let self else { + return + } + if let itemId { + self.itemInteraction.hiddenMedia = Set([itemId.id]) + } else { + self.itemInteraction.hiddenMedia = Set() + } + self.updateHiddenItems() + }) + navigationController.pushViewController(storyContainerScreen) }) } @@ -1043,14 +1147,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let _ = self }, toggleSelection: { [weak self] id, value in - guard let self else { + guard let self, let itemInteraction = self._itemInteraction else { return } - let _ = self + if var selectedIds = itemInteraction.selectedIds { + if value { + selectedIds.insert(id) + } else { + selectedIds.remove(id) + } + itemInteraction.selectedIds = selectedIds + self.selectedIdsPromise.set(selectedIds) + self.updateSelectedItems(animated: true) + } } ) //TODO:selection - //self.itemInteraction.selectedItemIds = + if isArchive { + self._itemInteraction?.selectedIds = Set() + } + self.itemGridBinding.itemInteraction = self._itemInteraction self.contextGestureContainerNode.isGestureEnabled = true self.contextGestureContainerNode.addSubnode(self.itemGrid) @@ -1136,6 +1252,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr strongSelf.itemGrid.cancelGestures() } + + self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: .stories))) /*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, peerId: peerId, messageTag: self.stateTag) |> deliverOnMainQueue).start(next: { [weak self] value in @@ -1385,6 +1503,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } + let title: String + if state.totalCount == 0 { + title = "" + } else if state.totalCount == 1 { + if self.isSaved { + title = "1 saved story" + } else { + title = "1 story" + } + } else { + if self.isSaved { + title = "\(state.totalCount) saved stories" + } else { + title = "\(state.totalCount) stories" + } + } + self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: .stories))) + let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) var mappedItems: [SparseItemGrid.Item] = [] @@ -1402,6 +1538,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) + + if totalCount == 0 { + totalCount = 100 + } Queue.mainQueue().async { [weak self] in guard let strongSelf = self else { @@ -1626,67 +1766,64 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + public func clearSelection() { + self.itemInteraction.selectedIds = Set() + self.selectedIdsPromise.set(Set()) + self.updateSelectedItems(animated: true) + } + public func updateSelectedMessages(animated: Bool) { - /*switch self.contentType { - case .files, .music, .voiceAndVideoMessages: - self.itemGrid.forEachVisibleItem { item in - guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams else { - return - } - if let item = itemView.item { - itemView.bind( - item: item, - presentationData: self.itemGridBinding.chatPresentationData, - context: self.itemGridBinding.context, - chatLocation: self.itemGridBinding.chatLocation, - interaction: self.itemGridBinding.listItemInteraction, - isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), - size: CGSize(width: size.width, height: itemView.bounds.height), - insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset) - ) - } - } - case .photo, .video, .photoOrVideo, .gifs: - self.itemGrid.forEachVisibleItem { item in - guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { - return - } - itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), animated: animated) + } + + private func updateSelectedItems(animated: Bool) { + self.itemGrid.forEachVisibleItem { item in + guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { + return } + itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated) + } - let isSelecting = self.chatControllerInteraction.selectionState != nil - self.itemGrid.pinchEnabled = !isSelecting - - if isSelecting { - if self.gridSelectionGesture == nil { - let selectionGesture = MediaPickerGridSelectionGesture() - selectionGesture.delegate = self - selectionGesture.sideInset = 44.0 - selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in - self?.itemGrid.isScrollEnabled = isEnabled - } - selectionGesture.itemAt = { [weak self] point in - if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id { - return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false) - } else { - return nil - } - } - selectionGesture.updateSelection = { [weak self] messageId, selected in - if let strongSelf = self { - strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected) - } - } - self.itemGrid.view.addGestureRecognizer(selectionGesture) - self.gridSelectionGesture = selectionGesture + /*let isSelecting = self.chatControllerInteraction.selectionState != nil + self.itemGrid.pinchEnabled = !isSelecting + + if isSelecting { + if self.gridSelectionGesture == nil { + let selectionGesture = MediaPickerGridSelectionGesture() + selectionGesture.delegate = self + selectionGesture.sideInset = 44.0 + selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in + self?.itemGrid.isScrollEnabled = isEnabled } - } else if let gridSelectionGesture = self.gridSelectionGesture { - self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture) - self.gridSelectionGesture = nil + selectionGesture.itemAt = { [weak self] point in + if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id { + return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false) + } else { + return nil + } + } + selectionGesture.updateSelection = { [weak self] messageId, selected in + if let strongSelf = self { + strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected) + } + } + self.itemGrid.view.addGestureRecognizer(selectionGesture) + self.gridSelectionGesture = selectionGesture } + } else if let gridSelectionGesture = self.gridSelectionGesture { + self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture) + self.gridSelectionGesture = nil }*/ } + private func updateHiddenItems() { + self.itemGrid.forEachVisibleItem { item in + guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { + return + } + itemLayer.isHidden = self.itemInteraction.hiddenMedia.contains(item.story.id) + } + } + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) @@ -1704,8 +1841,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } let fixedItemAspect: CGFloat? = 9.0 / 16.0 + + let gridTopInset = topInset - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index ee0c4c7e3e..1965fb7b77 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1144,7 +1144,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } } - func onTap(item: SparseItemGrid.Item) { + func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { return } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index 5354215835..48894fed16 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/CheckNode", "//submodules/Markdown", "//submodules/ContextUI", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 43115b1632..0a3f60fe09 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -23,6 +23,7 @@ import TelegramAnimatedStickerNode import TelegramStringFormatting import GalleryData import AnimatedTextComponent +import BottomButtonPanelComponent #if DEBUG import os.signpost @@ -1207,7 +1208,7 @@ final class StorageUsageScreenComponent: Component { let selectionPanelSize = selectionPanel.update( transition: selectionPanelTransition, - component: AnyComponent(StorageUsageScreenSelectionPanelComponent( + component: AnyComponent(BottomButtonPanelComponent( theme: environment.theme, title: bottomPanelSelectionData.isComplete ? environment.strings.StorageManagement_ClearCache : environment.strings.StorageManagement_ClearSelected, label: bottomPanelSelectionData.size == 0 ? nil : dataSizeString(Int(bottomPanelSelectionData.size), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 1ae7cdc108..bce2952bf0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -29,22 +29,63 @@ func hasFirstResponder(_ view: UIView) -> Bool { return false } +private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { + var updateIsTracking: ((Bool) -> Void)? + + override var state: UIGestureRecognizer.State { + didSet { + switch self.state { + case .began, .cancelled, .ended, .failed: + if self.isTracking { + self.isTracking = false + self.updateIsTracking?(false) + } + default: + break + } + } + } + + private var isTracking: Bool = false + + override func reset() { + super.reset() + + if self.isTracking { + self.isTracking = false + self.updateIsTracking?(false) + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if !self.isTracking { + self.isTracking = true + self.updateIsTracking?(true) + } + } +} + private final class StoryContainerScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let content: StoryContentContext + let focusedItemPromise: Promise let transitionIn: StoryContainerScreen.TransitionIn? let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut? init( context: AccountContext, content: StoryContentContext, + focusedItemPromise: Promise, transitionIn: StoryContainerScreen.TransitionIn?, transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut? ) { self.context = context self.content = content + self.focusedItemPromise = focusedItemPromise self.transitionIn = transitionIn self.transitionOut = transitionOut } @@ -118,12 +159,14 @@ private final class StoryContainerScreenComponent: Component { private let backgroundLayer: SimpleLayer private let backgroundEffectView: BlurredBackgroundView + private let focusedItem = ValuePromise(nil, ignoreRepeated: true) private var contentUpdatedDisposable: Disposable? private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:] private var itemSetPanState: ItemSetPanState? private var dismissPanState: ItemSetPanState? + private var isHoldingTouch: Bool = false private var isAnimatingOut: Bool = false private var didAnimateOut: Bool = false @@ -163,8 +206,15 @@ private final class StoryContainerScreenComponent: Component { }) self.addGestureRecognizer(verticalPanRecognizer) - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))) + let longPressRecognizer = StoryLongPressRecognizer(target: self, action: #selector(self.longPressGesture(_:))) longPressRecognizer.delegate = self + longPressRecognizer.updateIsTracking = { [weak self] isTracking in + guard let self else { + return + } + self.isHoldingTouch = isTracking + self.state?.updated(transition: .immediate) + } self.addGestureRecognizer(longPressRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) @@ -316,7 +366,7 @@ private final class StoryContainerScreenComponent: Component { } } - @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + @objc private func longPressGesture(_ recognizer: StoryLongPressRecognizer) { switch recognizer.state { case .began: if self.itemSetPanState == nil { @@ -381,6 +431,10 @@ private final class StoryContainerScreenComponent: Component { } func animateIn() { + if let component = self.component { + component.focusedItemPromise.set(self.focusedItem.get()) + } + if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil { self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) @@ -409,17 +463,22 @@ private final class StoryContainerScreenComponent: Component { transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0) let transitionOutCompleted = transitionOut.completed + let focusedItemPromise = component.focusedItemPromise itemSetComponentView.animateOut(transitionOut: transitionOut, completion: { completion() transitionOutCompleted() + focusedItemPromise.set(.single(nil)) }) } else { self.dismissPanState = ItemSetPanState(fraction: 1.0, didBegin: true) self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + let focusedItemPromise = self.component?.focusedItemPromise + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in completion() + focusedItemPromise?.set(.single(nil)) }) transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0) } @@ -475,6 +534,12 @@ private final class StoryContainerScreenComponent: Component { return } if update { + var focusedItemId: StoryId? + if let slice = component.content.stateValue?.slice { + focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id) + } + self.focusedItem.set(focusedItemId) + if component.content.stateValue?.slice == nil { self.environment?.controller()?.dismiss() } else { @@ -511,6 +576,9 @@ private final class StoryContainerScreenComponent: Component { if self.isAnimatingOut { isProgressPaused = true } + if self.isHoldingTouch { + isProgressPaused = true + } var dismissPanOffset: CGFloat = 0.0 var dismissPanScale: CGFloat = 1.0 @@ -684,9 +752,15 @@ private final class StoryContainerScreenComponent: Component { environment.controller()?.dismiss() } - let _ = component.context.engine.messages.deleteStory(id: slice.item.storyItem.id).start() + let _ = component.context.engine.messages.deleteStories(ids: [slice.item.storyItem.id]).start() } }, + markAsSeen: { [weak self] id in + guard let self, let component = self.component else { + return + } + component.content.markAsSeen(id: id) + }, controller: { [weak self] in return self?.environment?.controller() } @@ -865,6 +939,35 @@ private final class StoryContainerScreenComponent: Component { } public class StoryContainerScreen: ViewControllerComponentContainer { + public struct TransitionState: Equatable { + public var sourceSize: CGSize + public var destinationSize: CGSize + public var progress: CGFloat + + public init( + sourceSize: CGSize, + destinationSize: CGSize, + progress: CGFloat + ) { + self.sourceSize = sourceSize + self.destinationSize = destinationSize + self.progress = progress + } + } + + public final class TransitionView { + public let makeView: () -> UIView + public let updateView: (UIView, TransitionState, Transition) -> Void + + public init( + makeView: @escaping () -> UIView, + updateView: @escaping (UIView, TransitionState, Transition) -> Void + ) { + self.makeView = makeView + self.updateView = updateView + } + } + public final class TransitionIn { public weak var sourceView: UIView? public let sourceRect: CGRect @@ -883,6 +986,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { public final class TransitionOut { public weak var destinationView: UIView? + public let transitionView: TransitionView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat public let destinationIsAvatar: Bool @@ -890,12 +994,14 @@ public class StoryContainerScreen: ViewControllerComponentContainer { public init( destinationView: UIView, + transitionView: TransitionView?, destinationRect: CGRect, destinationCornerRadius: CGFloat, destinationIsAvatar: Bool, completed: @escaping () -> Void ) { self.destinationView = destinationView + self.transitionView = transitionView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius self.destinationIsAvatar = destinationIsAvatar @@ -906,6 +1012,11 @@ public class StoryContainerScreen: ViewControllerComponentContainer { private let context: AccountContext private var isDismissed: Bool = false + private let focusedItemPromise = Promise(nil) + public var focusedItem: Signal { + return self.focusedItemPromise.get() + } + public init( context: AccountContext, content: StoryContentContext, @@ -917,6 +1028,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { super.init(context: context, component: StoryContainerScreenComponent( context: context, content: content, + focusedItemPromise: self.focusedItemPromise, transitionIn: transitionIn, transitionOut: transitionOut ), navigationBarAppearance: .none, theme: .dark) @@ -925,6 +1037,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true self.automaticallyControlPresentationContextLayout = false + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: [.portrait]) self.context.sharedContext.hasPreloadBlockingContent.set(.single(true)) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 7fde8eaf80..ccfce0b14b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import SwiftSignalKit import TelegramCore +import Postbox public final class StoryContentItem { public final class ExternalState { @@ -22,13 +23,16 @@ public final class StoryContentItem { public final class Environment: Equatable { public let externalState: ExternalState public let presentationProgressUpdated: (Double, Bool) -> Void + public let markAsSeen: (StoryId) -> Void public init( externalState: ExternalState, - presentationProgressUpdated: @escaping (Double, Bool) -> Void + presentationProgressUpdated: @escaping (Double, Bool) -> Void, + markAsSeen: @escaping (StoryId) -> Void ) { self.externalState = externalState self.presentationProgressUpdated = presentationProgressUpdated + self.markAsSeen = markAsSeen } public static func ==(lhs: Environment, rhs: Environment) -> Bool { @@ -46,10 +50,6 @@ public final class StoryContentItem { public let rightInfoComponent: AnyComponent? public let peerId: EnginePeer.Id? public let storyItem: EngineStoryItem - public let preload: Signal? - public let delete: (() -> Void)? - public let markAsSeen: (() -> Void)? - public let hasLike: Bool public let isMy: Bool public init( @@ -60,10 +60,6 @@ public final class StoryContentItem { rightInfoComponent: AnyComponent?, peerId: EnginePeer.Id?, storyItem: EngineStoryItem, - preload: Signal?, - delete: (() -> Void)?, - markAsSeen: (() -> Void)?, - hasLike: Bool, isMy: Bool ) { self.id = id @@ -73,10 +69,6 @@ public final class StoryContentItem { self.rightInfoComponent = rightInfoComponent self.peerId = peerId self.storyItem = storyItem - self.preload = preload - self.delete = delete - self.markAsSeen = markAsSeen - self.hasLike = hasLike self.isMy = isMy } } @@ -183,4 +175,5 @@ public protocol StoryContentContext: AnyObject { func resetSideStates() func navigate(navigation: StoryContentContextNavigation) + func markAsSeen(id: StoryId) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 7fe6ce2b2c..9036d65892 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -51,6 +51,7 @@ public final class StoryItemSetContainerComponent: Component { public let close: () -> Void public let navigate: (NavigationDirection) -> Void public let delete: () -> Void + public let markAsSeen: (StoryId) -> Void public let controller: () -> ViewController? public init( @@ -71,6 +72,7 @@ public final class StoryItemSetContainerComponent: Component { close: @escaping () -> Void, navigate: @escaping (NavigationDirection) -> Void, delete: @escaping () -> Void, + markAsSeen: @escaping (StoryId) -> Void, controller: @escaping () -> ViewController? ) { self.context = context @@ -90,6 +92,7 @@ public final class StoryItemSetContainerComponent: Component { self.close = close self.navigate = navigate self.delete = delete + self.markAsSeen = markAsSeen self.controller = controller } @@ -489,31 +492,38 @@ public final class StoryItemSetContainerComponent: Component { self.visibleItems[focusedItem.id] = visibleItem } + let itemEnvironment = StoryContentItem.Environment( + externalState: visibleItem.externalState, + presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in + guard let self = self, let component = self.component else { + return + } + guard let visibleItem else { + return + } + visibleItem.currentProgress = progress + + if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View { + navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate) + } + if progress >= 1.0 && canSwitch && !visibleItem.requestedNext { + visibleItem.requestedNext = true + + component.navigate(.next) + } + }, + markAsSeen: { [weak self] id in + guard let self, let component = self.component else { + return + } + component.markAsSeen(id) + } + ) let _ = visibleItem.view.update( transition: itemTransition, component: focusedItem.component, environment: { - StoryContentItem.Environment( - externalState: visibleItem.externalState, - presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in - guard let self = self, let component = self.component else { - return - } - guard let visibleItem else { - return - } - visibleItem.currentProgress = progress - - if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View { - navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate) - } - if progress >= 1.0 && canSwitch && !visibleItem.requestedNext { - visibleItem.requestedNext = true - - component.navigate(.next) - } - } - ) + itemEnvironment }, containerSize: itemLayout.size ) @@ -678,6 +688,8 @@ public final class StoryItemSetContainerComponent: Component { let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self) let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size) + let contentSourceFrame = self.contentContainerView.frame + if let centerInfoView = self.centerInfoItem?.view.view { centerInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } @@ -703,6 +715,35 @@ public final class StoryItemSetContainerComponent: Component { removeOnCompletion: false ) + let transitionView = transitionOut.transitionView + let transitionViewImpl = transitionView?.makeView() + if let transitionViewImpl { + self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView) + + transitionViewImpl.frame = contentSourceFrame + transitionViewImpl.alpha = 0.0 + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 0.0 + ), .immediate) + } + + if let transitionViewImpl { + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + + transitionViewImpl.alpha = 1.0 + transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 1.0 + ), transition) + } + if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view { let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width @@ -755,7 +796,7 @@ public final class StoryItemSetContainerComponent: Component { } if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id { - let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id).start() + component.markAsSeen(StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)) } if self.topContentGradientLayer.colors == nil { @@ -1069,7 +1110,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.updateStoryIsPinned(id: component.slice.item.storyItem.id, isPinned: !component.slice.item.storyItem.isPinned).start() + 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) diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index c464c77630..15d64a0607 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -197,20 +197,6 @@ public final class StoryContentContextImpl: StoryContentContext { )), peerId: peer.id, storyItem: mappedItem, - preload: nil, - delete: { [weak context] in - guard let context else { - return - } - let _ = context - }, - markAsSeen: { [weak context] in - guard let context else { - return - } - let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start() - }, - hasLike: false, isMy: peerId == context.account.peerId ), totalCount: itemsView.items.count, @@ -727,6 +713,10 @@ public final class StoryContentContextImpl: StoryContentContext { } } } + + public func markAsSeen(id: StoryId) { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + } } public final class SingleStoryContentContextImpl: StoryContentContext { @@ -818,20 +808,6 @@ public final class SingleStoryContentContextImpl: StoryContentContext { )), peerId: peer.id, storyItem: mappedItem, - preload: nil, - delete: { [weak context] in - guard let context else { - return - } - let _ = context - }, - markAsSeen: { [weak context] in - guard let context else { - return - } - let _ = context.engine.messages.markStoryAsSeen(peerId: peer.id, id: item.id).start() - }, - hasLike: false, isMy: peer.id == context.account.peerId ), totalCount: 1, @@ -873,6 +849,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { public func navigate(navigation: StoryContentContextNavigation) { } + + public func markAsSeen(id: StoryId) { + } } public final class PeerStoryListContentContextImpl: StoryContentContext { @@ -899,6 +878,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { private var focusedId: Int32? private var focusedIdUpdated = Promise(Void()) + private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:] + private var pollStoryMetadataDisposables = DisposableSet() + public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) { self.context = context @@ -968,12 +950,6 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { )), peerId: peer.id, storyItem: item, - preload: nil, - delete: { - }, - markAsSeen: { - }, - hasLike: false, isMy: peerId == self.context.account.peerId ), totalCount: state.totalCount, @@ -997,6 +973,97 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { self.stateValue = stateValue self.statePromise.set(.single(stateValue)) self.updatedPromise.set(.single(Void())) + + var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:] + var pollItems: [StoryKey] = [] + + if let peer, let focusedIndex, let slice = stateValue.slice { + var possibleItems: [(EnginePeer, EngineStoryItem)] = [] + if peer.id == self.context.account.peerId { + pollItems.append(StoryKey(peerId: peer.id, id: slice.item.storyItem.id)) + } + + for i in focusedIndex ..< min(focusedIndex + 4, state.items.count) { + if i != focusedIndex { + possibleItems.append((slice.peer, state.items[i])) + } + + if slice.peer.id == self.context.account.peerId { + pollItems.append(StoryKey(peerId: slice.peer.id, id: state.items[i].id)) + } + } + + var nextPriority = 0 + for i in 0 ..< min(possibleItems.count, 3) { + let peer = possibleItems[i].0 + let item = possibleItems[i].1 + if let peerReference = PeerReference(peer._asPeer()) { + if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource { + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: nil, + priority: .top(position: nextPriority) + ) + nextPriority += 1 + } else if let file = item.media._asMedia() as? TelegramMediaFile { + if let preview = file.previewRepresentations.last { + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: nil, + priority: .top(position: nextPriority) + ) + nextPriority += 1 + } + + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: file.preloadSize, + priority: .top(position: nextPriority) + ) + nextPriority += 1 + } + } + } + } + + var validIds: [MediaResourceId] = [] + for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) { + let resource = info.resource + validIds.append(resource.resource.id) + if self.preloadStoryResourceDisposables[resource.resource.id] == nil { + var fetchRange: (Range, MediaBoxFetchPriority)? + if let size = info.size { + fetchRange = (0 ..< Int64(size), .default) + } + self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start() + } + } + + var removeIds: [MediaResourceId] = [] + for (id, disposable) in self.preloadStoryResourceDisposables { + if !validIds.contains(id) { + removeIds.append(id) + disposable.dispose() + } + } + for id in removeIds { + self.preloadStoryResourceDisposables.removeValue(forKey: id) + } + + var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:] + for storyKey in pollItems.prefix(3) { + if pollIdByPeerId[storyKey.peerId] == nil { + pollIdByPeerId[storyKey.peerId] = [storyKey.id] + } else { + pollIdByPeerId[storyKey.peerId]?.append(storyKey.id) + } + } + for (peerId, ids) in pollIdByPeerId { + self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start()) + } } }) } @@ -1004,6 +1071,11 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { deinit { self.storyDisposable?.dispose() self.requestStoryDisposables.dispose() + + for (_, disposable) in self.preloadStoryResourceDisposables { + disposable.dispose() + } + self.pollStoryMetadataDisposables.dispose() } public func resetSideStates() { @@ -1039,4 +1111,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } } } + + public func markAsSeen(id: StoryId) { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start() + } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index 192e7c2a12..8718b33d37 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import AccountContext import TelegramCore +import Postbox import AsyncDisplayKit import PhotoResources import SwiftSignalKit @@ -240,7 +241,7 @@ final class StoryItemContentComponent: Component { if !self.markedAsSeen { self.markedAsSeen = true if let component = self.component { - let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start() + self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id)) } } @@ -319,7 +320,7 @@ final class StoryItemContentComponent: Component { if !self.markedAsSeen { self.markedAsSeen = true if let component = self.component { - let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start() + self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id)) } } } diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json new file mode 100644 index 0000000000..23704b4f64 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Stories.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg new file mode 100644 index 0000000000..2d84bcf928 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1a2c70c56f..db4a381efb 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4564,6 +4564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let result = itemNode.targetForStoryTransition(id: storyId) { transitionOut = StoryContainerScreen.TransitionOut( destinationView: result, + transitionView: nil, destinationRect: result.bounds, destinationCornerRadius: 2.0, destinationIsAvatar: false, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 83bfb84f49..b48856bcdd 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -368,7 +368,7 @@ private final class PeerInfoPendingPane { let paneNode: PeerInfoPaneNode switch key { case .stories: - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isArchive: false, navigationController: chatControllerInteraction.navigationController) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index a643e20bd8..57c628e3bc 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -789,7 +789,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } //TODO:localize - items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stickers, action: { + items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stories, action: { interaction.openSettings(.stories) })) From 3a2f75ab828e57c5e9d5f3989c9f08cd74fd377e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 11:11:57 +0400 Subject: [PATCH 02/13] Story animation transitions --- .../Sources/ChatListController.swift | 19 ++-- submodules/ContactListUI/BUILD | 1 + .../Sources/ContactsController.swift | 6 +- submodules/TelegramUI/BUILD | 1 + .../Components/ChatListHeaderComponent/BUILD | 1 + .../Sources/ChatListHeaderComponent.swift | 1 + .../Components/MoreHeaderButton/BUILD | 20 +++++ .../Sources/MoreHeaderButton.swift | 0 .../PeerInfo/PeerInfoStoryGridScreen/BUILD | 1 + .../Sources/PeerInfoStoryGridScreen.swift | 1 + .../StoryItemSetContainerComponent.swift | 90 +++++++++++++------ .../Stories/StoryFooterPanelComponent/BUILD | 2 +- .../Sources/StoryFooterPanelComponent.swift | 2 +- .../Stories/StoryPeerListComponent/BUILD | 1 + .../Sources/StoryPeerListComponent.swift | 41 ++++++++- .../Sources/StoryPeerListItemComponent.swift | 63 ++++++++++++- .../TelegramUI/Sources/ChatController.swift | 1 + 17 files changed, 205 insertions(+), 46 deletions(-) create mode 100644 submodules/TelegramUI/Components/MoreHeaderButton/BUILD rename submodules/TelegramUI/Components/{ChatListHeaderComponent => MoreHeaderButton}/Sources/MoreHeaderButton.swift (100%) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 452b249cbd..d8faf72ab7 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2277,7 +2277,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController fileprivate func openStoryCamera() { var cameraTransitionIn: StoryCameraTransitionIn? if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { cameraTransitionIn = StoryCameraTransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, @@ -2292,7 +2292,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { return StoryCameraTransitionOut( destinationView: transitionView, destinationRect: transitionView.bounds, @@ -2340,7 +2340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var transitionIn: StoryContainerScreen.TransitionIn? if let peer, let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, @@ -2359,10 +2359,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { + if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryContainerScreen.TransitionOut( destinationView: transitionView, - transitionView: nil, + transitionView: transitionContentView, destinationRect: transitionView.bounds, destinationCornerRadius: transitionView.bounds.height * 0.5, destinationIsAvatar: true, @@ -2374,6 +2374,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } ) + if let componentView = self.chatListHeaderView() { + componentView.storyPeerListView()?.setPreviewedItem(signal: storyContainerScreen.focusedItem) + } self.push(storyContainerScreen) }) } @@ -2446,7 +2449,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController public func transitionViewForOwnStoryItem() -> UIView? { if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { return transitionView } } @@ -2455,7 +2458,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController public func animateStoryUploadRipple() { if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { let localRect = transitionView.convert(transitionView.bounds, to: self.view) self.animateRipple(centerLocation: localRect.center) } @@ -4778,7 +4781,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } if finished, let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { return StoryCameraTransitionOut( destinationView: transitionView, destinationRect: transitionView.bounds, diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 5cdac1cefe..fb00b0da66 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryContentComponent", "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", "//submodules/TelegramUI/Components/ChatListTitleView", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/ComponentFlow", ], visibility = [ diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index b4f9bf94be..cc14d02e07 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -529,7 +529,7 @@ public class ContactsController: ViewController { var transitionIn: StoryContainerScreen.TransitionIn? if let peer, let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, @@ -548,10 +548,10 @@ public class ContactsController: ViewController { } if let componentView = self.chatListHeaderView() { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { + if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryContainerScreen.TransitionOut( destinationView: transitionView, - transitionView: nil, + transitionView: transitionContentView, destinationRect: transitionView.bounds, destinationCornerRadius: transitionView.bounds.height * 0.5, destinationIsAvatar: true, diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 36964cc4f1..7441d5e1b9 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -372,6 +372,7 @@ swift_library( "//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", + "//submodules/TelegramUI/Components/MoreHeaderButton", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index 63d1a68cbf..6f94cb0c07 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/SearchUI", + "//submodules/TelegramUI/Components/MoreHeaderButton", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 6e7cce713c..0527392b00 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -8,6 +8,7 @@ import ChatListTitleView import AppBundle import StoryPeerListComponent import TelegramCore +import MoreHeaderButton public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { diff --git a/submodules/TelegramUI/Components/MoreHeaderButton/BUILD b/submodules/TelegramUI/Components/MoreHeaderButton/BUILD new file mode 100644 index 0000000000..de1deee74d --- /dev/null +++ b/submodules/TelegramUI/Components/MoreHeaderButton/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MoreHeaderButton", + module_name = "MoreHeaderButton", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/AnimationUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/MoreHeaderButton.swift b/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift similarity index 100% rename from submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/MoreHeaderButton.swift rename to submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index 0a656a777f..1abccac0f3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/ContextUI", "//submodules/UndoUI", "//submodules/TelegramUI/Components/BottomButtonPanelComponent", + "//submodules/TelegramUI/Components/MoreHeaderButton", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index be572d7296..d3d0c9fdba 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -13,6 +13,7 @@ import ContextUI import ChatTitleView import BottomButtonPanelComponent import UndoUI +import MoreHeaderButton final class PeerInfoStoryGridScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 9036d65892..92e11ea18c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -696,6 +696,42 @@ public final class StoryItemSetContainerComponent: Component { if let rightInfoView = self.rightInfoItem?.view.view { if transitionOut.destinationIsAvatar { + let transitionView = transitionOut.transitionView + let transitionViewImpl = transitionView?.makeView() + if let transitionViewImpl { + self.insertSubview(transitionViewImpl, aboveSubview: self.contentContainerView) + + let rightInfoSourceFrame = rightInfoView.convert(rightInfoView.bounds, to: self) + let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: sourceLocalFrame.center, to: rightInfoSourceFrame.center, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) + + transitionViewImpl.frame = rightInfoSourceFrame + transitionViewImpl.alpha = 0.0 + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: rightInfoSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 0.0 + ), .immediate) + + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + + transitionViewImpl.alpha = 1.0 + transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + rightInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + + transitionViewImpl.layer.position = positionKeyframes[positionKeyframes.count - 1] + transitionViewImpl.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) + transitionViewImpl.layer.animateBounds(from: CGRect(origin: CGPoint(), size: rightInfoSourceFrame.size), to: CGRect(origin: CGPoint(), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: rightInfoSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 1.0 + ), transition) + } + let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: rightInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) rightInfoView.layer.position = positionKeyframes[positionKeyframes.count - 1] rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) @@ -715,33 +751,33 @@ public final class StoryItemSetContainerComponent: Component { removeOnCompletion: false ) - let transitionView = transitionOut.transitionView - let transitionViewImpl = transitionView?.makeView() - if let transitionViewImpl { - self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView) - - transitionViewImpl.frame = contentSourceFrame - transitionViewImpl.alpha = 0.0 - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: contentSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 0.0 - ), .immediate) - } - - if let transitionViewImpl { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - - transitionViewImpl.alpha = 1.0 - transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - - transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: contentSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 1.0 - ), transition) + if !transitionOut.destinationIsAvatar { + let transitionView = transitionOut.transitionView + let transitionViewImpl = transitionView?.makeView() + if let transitionViewImpl { + self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView) + + transitionViewImpl.frame = contentSourceFrame + transitionViewImpl.alpha = 0.0 + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 0.0 + ), .immediate) + + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + + transitionViewImpl.alpha = 1.0 + transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 1.0 + ), transition) + } } if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view { diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD index e9688ac1be..08439b34a2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD @@ -14,10 +14,10 @@ swift_library( "//submodules/ComponentFlow", "//submodules/AppBundle", "//submodules/Components/BundleIconComponent", - "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/AnimatedAvatarSetNode", "//submodules/AccountContext", "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/MoreHeaderButton", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index ebbf40a49d..7d9a7e1fa6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -4,10 +4,10 @@ import Display import ComponentFlow import AppBundle import BundleIconComponent -import ChatListHeaderComponent import AnimatedAvatarSetNode import AccountContext import TelegramCore +import MoreHeaderButton public final class StoryFooterPanelComponent: Component { public let context: AccountContext diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index 5e5793b2d8..c192bd5d1d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AvatarNode", "//submodules/ContextUI", + "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 6c1f61890b..9573aa3168 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -6,8 +6,10 @@ import AppBundle import BundleIconComponent import AccountContext import TelegramCore +import Postbox import SwiftSignalKit import TelegramPresentationData +import StoryContainerScreen public final class StoryPeerListComponent: Component { public final class ExternalState { @@ -142,6 +144,9 @@ public final class StoryPeerListComponent: Component { private var requestedLoadMoreToken: String? private let loadMoreDisposable = MetaDisposable() + private var previewedItemDisposable: Disposable? + private var previewedItemId: EnginePeer.Id? + public override init(frame: CGRect) { self.collapsedButton = HighlightableButton() @@ -187,6 +192,7 @@ public final class StoryPeerListComponent: Component { deinit { self.loadMoreDisposable.dispose() + self.previewedItemDisposable?.dispose() } @objc private func collapsedButtonPressed() { @@ -196,12 +202,41 @@ public final class StoryPeerListComponent: Component { component.peerAction(nil) } - public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? { + public func setPreviewedItem(signal: Signal) { + self.previewedItemDisposable?.dispose() + self.previewedItemDisposable = (signal |> map(\.?.peerId) |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] itemId in + guard let self else { + return + } + self.previewedItemId = itemId + + for (peerId, visibleItem) in self.visibleItems { + if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { + itemView.updateIsPreviewing(isPreviewing: peerId == itemId) + } + } + }) + } + + public func transitionViewForItem(peerId: EnginePeer.Id) -> (UIView, StoryContainerScreen.TransitionView)? { if self.collapsedButton.isUserInteractionEnabled { return nil } if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { - return itemView.transitionView() + if !self.scrollView.bounds.intersects(itemView.frame) { + return nil + } + + return itemView.transitionView().flatMap { transitionView in + return (transitionView, StoryContainerScreen.TransitionView( + makeView: { [weak itemView] in + return StoryPeerListItemComponent.TransitionView(itemView: itemView) + }, + updateView: { view, state, transition in + (view as? StoryPeerListItemComponent.TransitionView)?.update(state: state, transition: transition) + } + )) + } } return nil } @@ -394,6 +429,8 @@ public final class StoryPeerListComponent: Component { itemTransition.setFrame(view: itemView.backgroundContainer, frame: itemFrame) itemTransition.setAlpha(view: itemView.backgroundContainer, alpha: itemAlpha) itemTransition.setScale(view: itemView.backgroundContainer, scale: itemScale) + + itemView.updateIsPreviewing(isPreviewing: self.previewedItemId == itemSet.peer.id) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index d7f6fce759..863081c0ac 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -11,6 +11,7 @@ import TelegramPresentationData import AvatarNode import ContextUI import AsyncDisplayKit +import StoryContainerScreen private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? { let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y) @@ -144,6 +145,47 @@ private final class StoryProgressLayer: SimpleShapeLayer { private var sharedAvatarBackgroundImage: UIImage? public final class StoryPeerListItemComponent: Component { + public final class TransitionView: UIView { + private weak var itemView: StoryPeerListItemComponent.View? + private var snapshotView: UIView? + private var portalView: PortalView? + + init(itemView: StoryPeerListItemComponent.View?) { + self.itemView = itemView + + super.init(frame: CGRect()) + + if let itemView { + if let portalView = PortalView(matchPosition: false) { + itemView.avatarContent.addPortal(view: portalView) + self.portalView = portalView + self.addSubview(portalView.view) + } + /*if let snapshotView = itemView.avatarContent.snapshotView(afterScreenUpdates: false) { + self.addSubview(snapshotView) + self.snapshotView = snapshotView + }*/ + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(state: StoryContainerScreen.TransitionState, transition: Transition) { + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + + if let snapshotView = self.snapshotView { + transition.setPosition(view: snapshotView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: snapshotView, scale: size.width / state.destinationSize.width) + } + if let portalView = self.portalView { + transition.setPosition(view: portalView.view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: portalView.view, scale: size.width / state.destinationSize.width) + } + } + } + public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings @@ -240,6 +282,7 @@ public final class StoryPeerListItemComponent: Component { private let button: HighlightTrackingButton + fileprivate let avatarContent: PortalSourceView private let avatarContainer: UIView private let avatarBackgroundContainer: UIView private let avatarBackgroundView: UIImageView @@ -266,6 +309,9 @@ public final class StoryPeerListItemComponent: Component { self.extractedBackgroundView = UIImageView() self.extractedBackgroundView.alpha = 0.0 + self.avatarContent = PortalSourceView() + self.avatarContent.isUserInteractionEnabled = false + self.avatarContainer = UIView() self.avatarContainer.isUserInteractionEnabled = false @@ -294,9 +340,10 @@ public final class StoryPeerListItemComponent: Component { self.avatarBackgroundContainer.addSubview(self.avatarBackgroundView) self.extractedContainerNode.contentNode.view.addSubview(self.button) - self.button.addSubview(self.avatarContainer) + self.avatarContent.addSubview(self.avatarContainer) + self.button.addSubview(self.avatarContent) - self.button.layer.addSublayer(self.indicatorColorLayer) + self.avatarContent.layer.addSublayer(self.indicatorColorLayer) self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer) self.indicatorColorLayer.mask = self.indicatorMaskLayer @@ -366,6 +413,10 @@ public final class StoryPeerListItemComponent: Component { return self.avatarNode?.view } + func updateIsPreviewing(isPreviewing: Bool) { + self.avatarContent.alpha = isPreviewing ? 0.0 : 1.0 + } + func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let size = availableSize @@ -439,7 +490,11 @@ public final class StoryPeerListItemComponent: Component { peer: component.peer ) avatarNode.updateSize(size: avatarSize) - transition.setPosition(view: self.avatarContainer, position: avatarFrame.center) + + transition.setPosition(view: self.avatarContent, position: CGPoint(x: avatarFrame.midX, y: avatarFrame.midY)) + transition.setBounds(view: self.avatarContent, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + + transition.setPosition(view: self.avatarContainer, position: CGPoint(x: avatarFrame.width * 0.5, y: avatarFrame.height * 0.5)) transition.setBounds(view: self.avatarContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) transition.setPosition(view: self.avatarBackgroundContainer, position: avatarFrame.center) @@ -517,7 +572,7 @@ public final class StoryPeerListItemComponent: Component { self.indicatorColorLayer.colors = colors } - transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.center) + transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center) transition.setBounds(layer: self.indicatorColorLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) transition.setPosition(layer: self.indicatorShapeLayer, position: CGPoint(x: indicatorFrame.width * 0.5, y: indicatorFrame.height * 0.5)) transition.setBounds(layer: self.indicatorShapeLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index db4a381efb..71c75df56b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -96,6 +96,7 @@ import LegacyCamera import LegacyInstantVideoController import StoryContainerScreen import StoryContentComponent +import MoreHeaderButton #if DEBUG import os.signpost From 9b7e421107792c41c8d6839ad85fc62c9d6c4beb Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 15:52:17 +0400 Subject: [PATCH 03/13] [WIP] Stories --- submodules/Display/Source/ListView.swift | 8 +- .../State/AccountStateManagementUtils.swift | 10 - .../SyncCore/SyncCore_Namespaces.swift | 1 + .../TelegramEngine/Messages/Stories.swift | 10 - .../Messages/StoryListContext.swift | 820 ++++++++++++------ .../Sources/PeerInfoStoryGridScreen.swift | 3 +- .../Sources/PeerInfoStoryPaneNode.swift | 4 +- .../Sources/PeerInfo/PeerInfoData.swift | 42 +- .../PeerInfo/PeerInfoPaneContainerNode.swift | 2 +- 9 files changed, 615 insertions(+), 285 deletions(-) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index e74e1645de..956260481b 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -3493,7 +3493,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture headerNodesTransition.0.animatePositionAdditive(node: topItemOverscrollBackground, offset: CGPoint(x: 0.0, y: -headerNodesTransition.2)) } - self.updateVisibleContentOffset() + if !self.useMainQueueTransactions { + self.updateVisibleContentOffset() + } if self.debugInfo { //let delta = CACurrentMediaTime() - timestamp @@ -3501,6 +3503,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } completion() + + if self.useMainQueueTransactions { + self.updateVisibleContentOffset() + } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a29649a712..a9e084c183 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4351,10 +4351,6 @@ func replayFinalState( } } - var filteredSubscriptionsOpaqueState: String? - if let state = transaction.getSubscriptionsStoriesState(key: .filtered)?.get(Stories.SubscriptionsState.self) { - filteredSubscriptionsOpaqueState = state.opaqueState - } var appliedMaxReadId: Int32? if let currentState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) { if let appliedMaxReadIdValue = appliedMaxReadId { @@ -4366,7 +4362,6 @@ func replayFinalState( transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( - subscriptionsOpaqueState: filteredSubscriptionsOpaqueState, maxReadId: appliedMaxReadId ?? 0 ))) @@ -4381,12 +4376,7 @@ func replayFinalState( appliedMaxReadId = max(appliedMaxReadId, currentState.maxReadId) } - var filteredSubscriptionsOpaqueState: String? - if let state = transaction.getSubscriptionsStoriesState(key: .filtered)?.get(Stories.SubscriptionsState.self) { - filteredSubscriptionsOpaqueState = state.opaqueState - } transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( - subscriptionsOpaqueState: filteredSubscriptionsOpaqueState, maxReadId: appliedMaxReadId ))) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index b93e375cbf..f898c70db9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -106,6 +106,7 @@ public struct Namespaces { public static let featuredStickersConfiguration: Int8 = 24 public static let emojiSearchCategories: Int8 = 25 public static let cachedEmojiQueryResults: Int8 = 26 + public static let cachedPeerStoryListHeads: Int8 = 27 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 277a9e09d0..50c57f475b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -352,39 +352,30 @@ public enum Stories { public final class PeerState: Equatable, Codable { private enum CodingKeys: CodingKey { - case subscriptionsOpaqueState case maxReadId } - public let subscriptionsOpaqueState: String? public let maxReadId: Int32 public init( - subscriptionsOpaqueState: String?, maxReadId: Int32 ){ - self.subscriptionsOpaqueState = subscriptionsOpaqueState self.maxReadId = maxReadId } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.subscriptionsOpaqueState = try container.decodeIfPresent(String.self, forKey: .subscriptionsOpaqueState) self.maxReadId = try container.decode(Int32.self, forKey: .maxReadId) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(self.subscriptionsOpaqueState, forKey: .subscriptionsOpaqueState) try container.encode(self.maxReadId, forKey: .maxReadId) } public static func ==(lhs: PeerState, rhs: PeerState) -> Bool { - if lhs.subscriptionsOpaqueState != rhs.subscriptionsOpaqueState { - return false - } if lhs.maxReadId != rhs.maxReadId { return false } @@ -864,7 +855,6 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPi return account.postbox.transaction { transaction -> Api.InputUser? in if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) { transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( - subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState, maxReadId: max(peerStoryState.maxReadId, id) ))) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index e88a71a258..370a7f698c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -318,15 +318,6 @@ public final class StorySubscriptionsContext { var updatedPeerEntries: [StoryItemsTableEntry] = [] for story in stories { if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) { - /*#if DEBUG - if "".isEmpty { - if let codedEntry = CodableEntry(Stories.StoredItem.placeholder(Stories.Placeholder(id: storedItem.id, timestamp: storedItem.timestamp))) { - updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) - } - continue - } - #endif*/ - if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) { updatedPeerEntries.append(previousEntry) } else { @@ -341,7 +332,6 @@ public final class StorySubscriptionsContext { transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( - subscriptionsOpaqueState: state, maxReadId: maxReadId ?? 0 ))) } @@ -415,122 +405,470 @@ public final class StorySubscriptionsContext { } } +private final class CachedPeerStoryListHead: Codable { + let items: [Stories.StoredItem] + let totalCount: Int32 + + init(items: [Stories.StoredItem], totalCount: Int32) { + self.items = items + self.totalCount = totalCount + } +} + public final class PeerStoryListContext { + private final class Impl { + private let queue: Queue + private let account: Account + private let peerId: EnginePeer.Id + private let isArchived: Bool + + private let statePromise = Promise() + private var stateValue: State { + didSet { + self.statePromise.set(.single(self.stateValue)) + } + } + var state: Signal { + return self.statePromise.get() + } + + private var isLoadingMore: Bool = false + private var requestDisposable: Disposable? + + private var updatesDisposable: Disposable? + + init(queue: Queue, account: Account, peerId: EnginePeer.Id, isArchived: Bool) { + self.queue = queue + self.account = account + self.peerId = peerId + self.isArchived = isArchived + + self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0, isCached: true) + + let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], Int) in + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) + guard let cached = cached else { + return (nil, [], 0) + } + var items: [EngineStoryItem] = [] + for storedItem in cached.items { + if case let .item(item) = storedItem, let media = item.media { + let mappedItem = EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + items.append(mappedItem) + } + } + + let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init) + + return (peerReference, items, Int(cached.totalCount)) + } + |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount in + guard let self else { + return + } + + self.stateValue = State(peerReference: peerReference, items: items, totalCount: totalCount, loadMoreToken: 0, isCached: true) + self.loadMore() + }) + } + + deinit { + self.requestDisposable?.dispose() + } + + func loadMore() { + if self.isLoadingMore { + return + } + guard let loadMoreToken = self.stateValue.loadMoreToken else { + return + } + + self.isLoadingMore = true + + let peerId = self.peerId + let account = self.account + let isArchived = self.isArchived + self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(peerId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in + guard let inputUser = inputUser else { + return .single(([], 0, nil)) + } + + let signal: Signal + if isArchived { + signal = account.network.request(Api.functions.stories.getStoriesArchive(offsetId: Int32(loadMoreToken), limit: 100)) + } else { + signal = account.network.request(Api.functions.stories.getPinnedStories(userId: inputUser, offsetId: Int32(loadMoreToken), limit: 100)) + } + return signal + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in + guard let result = result else { + return .single(([], 0, nil)) + } + + return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in + var storyItems: [EngineStoryItem] = [] + var totalCount: Int = 0 + + switch result { + case let .stories(count, stories, users): + totalCount = Int(count) + + var peers: [Peer] = [] + var peerPresences: [PeerId: Api.User] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + peerPresences[telegramUser.id] = user + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + for story in stories { + if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) { + if case let .item(item) = storedItem, let media = item.media { + let mappedItem = EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + storyItems.append(mappedItem) + } + } + } + + if loadMoreToken == 0 { + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.asStoryItem()) }, totalCount: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + } + } + + return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init)) + } + } + } + |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference in + guard let `self` = self else { + return + } + + self.isLoadingMore = false + + var updatedState = self.stateValue + if updatedState.isCached { + updatedState.items.removeAll() + updatedState.isCached = false + } + + var existingIds = Set(updatedState.items.map { $0.id }) + for item in storyItems { + if existingIds.contains(item.id) { + continue + } + existingIds.insert(item.id) + + updatedState.items.append(item) + } + + if updatedState.peerReference == nil { + updatedState.peerReference = peerReference + } + + updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init) + if updatedState.loadMoreToken != nil { + updatedState.totalCount = max(totalCount, updatedState.items.count) + } else { + updatedState.totalCount = updatedState.items.count + } + self.stateValue = updatedState + + if self.updatesDisposable == nil { + self.updatesDisposable = (self.account.stateManager.storyUpdates + |> deliverOn(self.queue)).start(next: { [weak self] updates in + guard let `self` = self else { + return + } + let selfPeerId = self.peerId + let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in + var peers: [PeerId: Peer] = [:] + + for update in updates { + switch update { + case let .added(peerId, item): + if selfPeerId == peerId { + if case let .item(item) = item { + if let views = item.views { + for id in views.seenPeerIds { + if let peer = transaction.getPeer(id) { + peers[peer.id] = peer + } + } + } + } + } + default: + break + } + } + + return peers + } + |> deliverOn(self.queue)).start(next: { [weak self] peers in + guard let `self` = self else { + return + } + + var finalUpdatedState: State? + + for update in updates { + switch update { + case let .deleted(peerId, id): + if self.peerId == peerId { + if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.remove(at: index) + updatedState.totalCount = max(0, updatedState.totalCount - 1) + finalUpdatedState = updatedState + } + } + case let .added(peerId, item): + if self.peerId == peerId { + if let index = self.stateValue.items.firstIndex(where: { $0.id == item.id }) { + if !self.isArchived { + if case let .item(item) = item { + if item.isPinned { + if let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items[index] = EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + finalUpdatedState = updatedState + } + } else { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.remove(at: index) + updatedState.totalCount = max(0, updatedState.totalCount - 1) + finalUpdatedState = updatedState + } + } + } + } else { + if !self.isArchived { + if case let .item(item) = item { + if item.isPinned { + if let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.append(EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + )) + updatedState.items.sort(by: { lhs, rhs in + return lhs.timestamp > rhs.timestamp + }) + finalUpdatedState = updatedState + } + } + } + } + } + } + case .read: + break + } + } + + if let finalUpdatedState = finalUpdatedState { + self.stateValue = finalUpdatedState + + let items = finalUpdatedState.items + let totalCount = finalUpdatedState.totalCount + let _ = (self.account.postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.asStoryItem()) }, totalCount: Int32(totalCount))) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + }).start() + } + }) + }) + } + }) + } + } + public struct State: Equatable { public var peerReference: PeerReference? public var items: [EngineStoryItem] public var totalCount: Int public var loadMoreToken: Int? + public var isCached: Bool init( peerReference: PeerReference?, items: [EngineStoryItem], totalCount: Int, - loadMoreToken: Int? + loadMoreToken: Int?, + isCached: Bool ) { self.peerReference = peerReference self.items = items self.totalCount = totalCount self.loadMoreToken = loadMoreToken + self.isCached = isCached } } - private let account: Account - private let peerId: EnginePeer.Id - private let isArchived: Bool - - private let statePromise = Promise() - private var stateValue: State { - didSet { - self.statePromise.set(.single(self.stateValue)) - } - } public var state: Signal { - return self.statePromise.get() + return impl.signalWith { impl, subscriber in + return impl.state.start(next: subscriber.putNext) + } } - private var isLoadingMore: Bool = false - private var requestDisposable: Disposable? - - private var updatesDisposable: Disposable? + private let queue: Queue + private let impl: QueueLocalObject public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) { - self.account = account - self.peerId = peerId - self.isArchived = isArchived - - self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0) - self.statePromise.set(.single(self.stateValue)) - - self.loadMore() - } - - deinit { - self.requestDisposable?.dispose() + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId, isArchived: isArchived) + }) } public func loadMore() { - if self.isLoadingMore { - return - } - guard let loadMoreToken = self.stateValue.loadMoreToken else { - return + self.impl.with { impl in + impl.loadMore() } + } +} + +public final class PeerExpiringStoryListContext { + private final class Impl { + private let queue: Queue + private let account: Account + private let peerId: EnginePeer.Id - self.isLoadingMore = true + private var listDisposable: Disposable? + private var pollDisposable: Disposable? - let peerId = self.peerId - let account = self.account - let isArchived = self.isArchived - self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in - return transaction.getPeer(peerId).flatMap(apiInputUser) - } - |> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in - guard let inputUser = inputUser else { - return .single(([], 0, nil)) - } + private let statePromise = Promise() + + init(queue: Queue, account: Account, peerId: EnginePeer.Id) { + self.queue = queue + self.account = account + self.peerId = peerId - let signal: Signal - if isArchived { - signal = account.network.request(Api.functions.stories.getStoriesArchive(offsetId: Int32(loadMoreToken), limit: 100)) - } else { - signal = account.network.request(Api.functions.stories.getPinnedStories(userId: inputUser, offsetId: Int32(loadMoreToken), limit: 100)) - } - return signal - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in - guard let result = result else { - return .single(([], 0, nil)) + self.listDisposable = (account.postbox.combinedView(keys: [ + PostboxViewKey.storiesState(key: .peer(peerId)), + PostboxViewKey.storyItems(peerId: peerId) + ]) + |> deliverOn(self.queue)).start(next: { [weak self] views in + guard let self else { + return + } + guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else { + return + } + guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { + return } - return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in - var storyItems: [EngineStoryItem] = [] - var totalCount: Int = 0 + let _ = (self.account.postbox.transaction { transaction -> State? in + let state = stateView.value?.get(Stories.PeerState.self) - switch result { - case let .stories(count, stories, users): - totalCount = Int(count) - - var peers: [Peer] = [] - var peerPresences: [PeerId: Api.User] = [:] - - for user in users { - let telegramUser = TelegramUser(user: user) - peers.append(telegramUser) - peerPresences[telegramUser.id] = user - } - - updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in - return updated - }) - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) - - for story in stories { - if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) { - if case let .item(item) = storedItem, let media = item.media { + var items: [Item] = [] + for item in itemsView.items { + if let item = item.value.get(Stories.StoredItem.self) { + switch item { + case let .item(item): + if let media = item.media { let mappedItem = EngineStoryItem( id: item.id, timestamp: item.timestamp, @@ -551,180 +889,156 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic ) - storyItems.append(mappedItem) + items.append(.item(mappedItem)) } + case let .placeholder(placeholder): + items.append(.placeholder(id: placeholder.id, timestamp: placeholder.timestamp, expirationTimestamp: placeholder.expirationTimestamp)) } } } - return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init)) + return State( + items: items, + isCached: false, + maxReadId: state?.maxReadId ?? 0 + ) } + |> deliverOn(self.queue)).start(next: { [weak self] state in + guard let self else { + return + } + guard let state else { + return + } + self.statePromise.set(.single(state)) + }) + }) + + self.poll() + } + + deinit { + self.listDisposable?.dispose() + self.pollDisposable?.dispose() + } + + private func poll() { + self.pollDisposable?.dispose() + + let account = self.account + let peerId = self.peerId + self.pollDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(peerId).flatMap(apiInputUser) } - }).start(next: { [weak self] storyItems, totalCount, peerReference in - guard let `self` = self else { - return - } - - self.isLoadingMore = false - - var updatedState = self.stateValue - - var existingIds = Set(updatedState.items.map { $0.id }) - for item in storyItems { - if existingIds.contains(item.id) { - continue + |> mapToSignal { inputUser -> Signal in + guard let inputUser else { + return .complete() } - existingIds.insert(item.id) - - updatedState.items.append(item) - } - - if updatedState.peerReference == nil { - updatedState.peerReference = peerReference - } - - updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init) - if updatedState.loadMoreToken != nil { - updatedState.totalCount = max(totalCount, updatedState.items.count) - } else { - updatedState.totalCount = updatedState.items.count - } - self.stateValue = updatedState - - if self.updatesDisposable == nil { - self.updatesDisposable = (self.account.stateManager.storyUpdates - |> deliverOnMainQueue).start(next: { [weak self] updates in + return account.network.request(Api.functions.stories.getUserStories(userId: inputUser)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + var updatedPeerEntries: [StoryItemsTableEntry] = [] + updatedPeerEntries.removeAll() + + if let result = result, case let .userStories(stories, users) = result { + var peers: [Peer] = [] + var peerPresences: [PeerId: Api.User] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + peerPresences[telegramUser.id] = user + } + + switch stories { + case let .userStories(_, userId, maxReadId, stories): + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + + let previousPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId) + + for story in stories { + if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) { + if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) { + updatedPeerEntries.append(previousEntry) + } else { + if let codedEntry = CodableEntry(storedItem) { + updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) + } + } + } + } + + transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState( + maxReadId: maxReadId ?? 0 + ))) + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + } + + transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) + } + |> ignoreValues + } + }).start(completed: { [weak self] in + guard let `self` = self else { + return + } + self.pollDisposable = (Signal.complete() |> suspendAwareDelay(60.0, queue: self.queue) |> deliverOn(self.queue)).start(completed: { [weak self] in guard let `self` = self else { return } - let selfPeerId = self.peerId - let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in - var peers: [PeerId: Peer] = [:] - - for update in updates { - switch update { - case let .added(peerId, item): - if selfPeerId == peerId { - if case let .item(item) = item { - if let views = item.views { - for id in views.seenPeerIds { - if let peer = transaction.getPeer(id) { - peers[peer.id] = peer - } - } - } - } - } - default: - break - } - } - - return peers - } - |> deliverOnMainQueue).start(next: { [weak self] peers in - guard let `self` = self else { - return - } - - var finalUpdatedState: State? - - for update in updates { - switch update { - case let .deleted(peerId, id): - if self.peerId == peerId { - if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) { - var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items.remove(at: index) - updatedState.totalCount = max(0, updatedState.totalCount - 1) - finalUpdatedState = updatedState - } - } - case let .added(peerId, item): - if self.peerId == peerId { - if let index = self.stateValue.items.firstIndex(where: { $0.id == item.id }) { - if !self.isArchived { - if case let .item(item) = item { - if item.isPinned { - if let media = item.media { - var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items[index] = EngineStoryItem( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: EngineMedia(media), - text: item.text, - entities: item.entities, - views: item.views.flatMap { views in - return EngineStoryItem.Views( - seenCount: views.seenCount, - seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in - return peers[id].flatMap(EnginePeer.init) - } - ) - }, - privacy: item.privacy.flatMap(EngineStoryPrivacy.init), - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic - ) - finalUpdatedState = updatedState - } - } else { - var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items.remove(at: index) - updatedState.totalCount = max(0, updatedState.totalCount - 1) - finalUpdatedState = updatedState - } - } - } - } else { - if !self.isArchived { - if case let .item(item) = item { - if item.isPinned { - if let media = item.media { - var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items.append(EngineStoryItem( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: EngineMedia(media), - text: item.text, - entities: item.entities, - views: item.views.flatMap { views in - return EngineStoryItem.Views( - seenCount: views.seenCount, - seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in - return peers[id].flatMap(EnginePeer.init) - } - ) - }, - privacy: item.privacy.flatMap(EngineStoryPrivacy.init), - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic - )) - updatedState.items.sort(by: { lhs, rhs in - return lhs.timestamp > rhs.timestamp - }) - finalUpdatedState = updatedState - } - } - } - } - } - } - case .read: - break - } - } - - if let finalUpdatedState = finalUpdatedState { - self.stateValue = finalUpdatedState - } - }) + self.poll() }) + }) + } + } + + public enum Item: Equatable { + case item(EngineStoryItem) + case placeholder(id: Int32, timestamp: Int32, expirationTimestamp: Int32) + } + + public final class State: Equatable { + public let items: [Item] + public let isCached: Bool + public let maxReadId: Int32 + + public init(items: [Item], isCached: Bool, maxReadId: Int32) { + self.items = items + self.isCached = isCached + self.maxReadId = maxReadId + } + + public static func ==(lhs: State, rhs: State) -> Bool { + if lhs === rhs { + return true } + if lhs.items != rhs.items { + return false + } + if lhs.maxReadId != rhs.maxReadId { + return false + } + return true + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public init(account: Account, peerId: EnginePeer.Id) { + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId) }) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index d3d0c9fdba..f6d37aa4e8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -386,7 +386,8 @@ final class PeerInfoStoryGridScreenComponent: Component { return nil } return self.environment?.controller()?.navigationController as? NavigationController - } + }, + listContext: nil ) self.paneNode = paneNode self.addSubview(paneNode.view) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index f74332837a..ad23fe4935 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -890,7 +890,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) { + public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context self.peerId = peerId self.chatLocation = chatLocation @@ -913,7 +913,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr captureProtected: captureProtected ) - self.listSource = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: self.isArchive) + self.listSource = listContext ?? PeerStoryListContext(account: context.account, peerId: peerId, isArchived: self.isArchive) self.calendarSource = nil super.init() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index e4b8ee529e..0808bd8d2d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -192,6 +192,7 @@ final class PeerInfoScreenData { let groupsInCommon: GroupsInCommonContext? let linkedDiscussionPeer: Peer? let members: PeerInfoMembersData? + let storyListContext: PeerStoryListContext? let encryptionKeyFingerprint: SecretChatKeyFingerprint? let globalSettings: TelegramGlobalSettings? let invitations: PeerExportedInvitationsState? @@ -214,6 +215,7 @@ final class PeerInfoScreenData { groupsInCommon: GroupsInCommonContext?, linkedDiscussionPeer: Peer?, members: PeerInfoMembersData?, + storyListContext: PeerStoryListContext?, encryptionKeyFingerprint: SecretChatKeyFingerprint?, globalSettings: TelegramGlobalSettings?, invitations: PeerExportedInvitationsState?, @@ -235,6 +237,7 @@ final class PeerInfoScreenData { self.groupsInCommon = groupsInCommon self.linkedDiscussionPeer = linkedDiscussionPeer self.members = members + self.storyListContext = storyListContext self.encryptionKeyFingerprint = encryptionKeyFingerprint self.globalSettings = globalSettings self.invitations = invitations @@ -390,10 +393,22 @@ func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, chatLoca case .none, .settings: return .complete() case .user, .channel, .group: - return combineLatest( - context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder) |> ignoreValues), - context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> ignoreValues - ) + var signals: [Signal] = [] + + signals.append(context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder) |> ignoreValues) |> ignoreValues) + signals.append(context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> ignoreValues) + + if case .user = inputData { + signals.append(Signal { _ in + let listContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) + + return ActionDisposable { + let _ = listContext + } + }) + } + + return combineLatest(signals) |> ignoreValues } } @@ -529,6 +544,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, groupsInCommon: nil, linkedDiscussionPeer: nil, members: nil, + storyListContext: nil, encryptionKeyFingerprint: nil, globalSettings: globalSettings, invitations: nil, @@ -561,6 +577,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon: nil, linkedDiscussionPeer: nil, members: nil, + storyListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: nil, @@ -667,17 +684,25 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen secretChatKeyFingerprint = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.SecretChatKeyFingerprint(id: secretChatId)) } + let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) + let hasStories: Signal = storyListContext.state + |> map { state -> Bool in + return !state.items.isEmpty + } + |> distinctUntilChanged + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), secretChatKeyFingerprint, - status + status, + hasStories ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories -> PeerInfoScreenData in var availablePanes = availablePanes - if peerView.peers[peerView.peerId] is TelegramUser { + if hasStories, peerView.peers[peerView.peerId] is TelegramUser { availablePanes?.insert(.stories, at: 0) } @@ -700,6 +725,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon: groupsInCommon, linkedDiscussionPeer: nil, members: nil, + storyListContext: storyListContext, encryptionKeyFingerprint: encryptionKeyFingerprint, globalSettings: nil, invitations: nil, @@ -779,6 +805,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon: nil, linkedDiscussionPeer: discussionPeer, members: nil, + storyListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: invitations, @@ -982,6 +1009,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon: nil, linkedDiscussionPeer: discussionPeer, members: membersData, + storyListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: invitations, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index b48856bcdd..ffb8b71625 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -368,7 +368,7 @@ private final class PeerInfoPendingPane { let paneNode: PeerInfoPaneNode switch key { case .stories: - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController, listContext: data.storyListContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() From 64c2556e2fb24fb3286143f739f2737a63c90194 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Fri, 9 Jun 2023 16:04:00 +0400 Subject: [PATCH 04/13] state for PeerExpiringStoryListContext --- .../Messages/StoryListContext.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 370a7f698c..64ce4f4b01 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -838,7 +838,7 @@ public final class PeerExpiringStoryListContext { private var listDisposable: Disposable? private var pollDisposable: Disposable? - private let statePromise = Promise() + fileprivate let statePromise = Promise() init(queue: Queue, account: Account, peerId: EnginePeer.Id) { self.queue = queue @@ -1041,4 +1041,17 @@ public final class PeerExpiringStoryListContext { return Impl(queue: queue, account: account, peerId: peerId) }) } + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.statePromise.get().start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + } From 7015298cd53bd64bedd2950e06c046ac73f0de6a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 16:04:42 +0400 Subject: [PATCH 05/13] [WIP] Stories --- .../Messages/StoryListContext.swift | 9 +++++++++ .../StoryItemSetContainerComponent.swift | 20 ++++++++++++++----- .../Sources/PeerInfo/PeerInfoData.swift | 2 ++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 370a7f698c..70b0b01e1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -839,6 +839,9 @@ public final class PeerExpiringStoryListContext { private var pollDisposable: Disposable? private let statePromise = Promise() + var state: Signal { + return self.statePromise.get() + } init(queue: Queue, account: Account, peerId: EnginePeer.Id) { self.queue = queue @@ -1034,6 +1037,12 @@ public final class PeerExpiringStoryListContext { private let queue: Queue private let impl: QueueLocalObject + public var state: Signal { + return impl.signalWith { impl, subscriber in + return impl.state.start(next: subscriber.putNext) + } + } + public init(account: Account, peerId: EnginePeer.Id) { let queue = Queue.mainQueue() self.queue = queue diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 92e11ea18c..8fcfea487d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -472,6 +472,19 @@ public final class StoryItemSetContainerComponent: Component { return super.hitTest(point, with: event) } + private func isProgressPaused() -> Bool { + guard let component = self.component else { + return false + } + if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList { + return true + } + if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 { + return true + } + return false + } + private func updateScrolling(transition: Transition) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -535,7 +548,7 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size)) if let view = view as? StoryContentItem.View { - view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList) + view.setIsProgressPaused(self.isProgressPaused()) } } @@ -554,13 +567,10 @@ public final class StoryItemSetContainerComponent: Component { } func updateIsProgressPaused() { - guard let component = self.component else { - return - } for (_, visibleItem) in self.visibleItems { if let view = visibleItem.view.view { if let view = view as? StoryContentItem.View { - view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList || self.isEditingStory) + view.setIsProgressPaused(self.isProgressPaused()) } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 0808bd8d2d..c7a02057d3 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -401,9 +401,11 @@ func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, chatLoca if case .user = inputData { signals.append(Signal { _ in let listContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) + let expiringListContext = PeerExpiringStoryListContext(account: context.account, peerId: peerId) return ActionDisposable { let _ = listContext + let _ = expiringListContext } }) } From 1ffc683a82f13a484772471ffde56dcf52cf9a6f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 16:42:24 +0400 Subject: [PATCH 06/13] [WIP] Stories --- .../Sources/ChatListController.swift | 4 +- .../Sources/ContactsController.swift | 2 +- .../Sources/StoryChatContent.swift | 200 +++++++++++++----- .../Sources/PeerInfo/PeerInfoScreen.swift | 70 +++++- 4 files changed, 219 insertions(+), 57 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d8faf72ab7..6fd78b7dc6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1244,7 +1244,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peerId) + let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peerId, singlePeer: false) let _ = (storyContent.state |> filter { $0.slice != nil } |> take(1) @@ -2325,7 +2325,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peer?.id) + let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peer?.id, singlePeer: false) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] storyContentState in diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index cc14d02e07..16ad97877a 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -515,7 +515,7 @@ public class ContactsController: ViewController { return } - let storyContent = StoryContentContextImpl(context: self.context, includeHidden: true, focusedPeerId: peer?.id) + let storyContent = StoryContentContextImpl(context: self.context, includeHidden: true, focusedPeerId: peer?.id, singlePeer: false) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] storyContentState in diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 15d64a0607..a7f5954fdd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -328,10 +328,13 @@ public final class StoryContentContextImpl: StoryContentContext { private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:] private var pollStoryMetadataDisposables = DisposableSet() + private var singlePeerListContext: PeerExpiringStoryListContext? + public init( context: AccountContext, includeHidden: Bool, - focusedPeerId: EnginePeer.Id? + focusedPeerId: EnginePeer.Id?, + singlePeer: Bool ) { self.context = context self.includeHidden = includeHidden @@ -339,73 +342,163 @@ public final class StoryContentContextImpl: StoryContentContext { self.focusedItem = (focusedPeerId, nil) } - self.storySubscriptionsDisposable = (context.engine.messages.storySubscriptions(includeHidden: includeHidden) - |> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in - guard let self else { + if singlePeer { + guard let focusedPeerId else { + assertionFailure() return } - - let startedWithUnseen: Bool - if let current = self.startedWithUnseen { - startedWithUnseen = current - } else { - var startedWithUnseenValue = false + let singlePeerListContext = PeerExpiringStoryListContext(account: context.account, peerId: focusedPeerId) + self.singlePeerListContext = singlePeerListContext + self.storySubscriptionsDisposable = (combineLatest( + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: focusedPeerId)) + singlePeerListContext.state + ) + |> deliverOnMainQueue).start(next: { [weak self] peer, state in + guard let self, let peer else { + return + } - if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId { + let storySubscriptions = EngineStorySubscriptions( + accountItem: nil, + items: [EngineStorySubscriptions.Item( + peer: peer, + hasUnseen: state.hasUnseen, + storyCount: state.items.count, + lastTimestamp: state.items.last?.timestamp ?? 0 + )], + hasMoreToken: nil + ) + + let startedWithUnseen: Bool + if let current = self.startedWithUnseen { + startedWithUnseen = current } else { - var centralIndex: Int? - if let (focusedPeerId, _) = self.focusedItem { - if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) { - centralIndex = index + var startedWithUnseenValue = false + + if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId { + } else { + var centralIndex: Int? + if let (focusedPeerId, _) = self.focusedItem { + if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) { + centralIndex = index + } } - } - if centralIndex == nil { - if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { - centralIndex = index + if centralIndex == nil { + if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { + centralIndex = index + } } - } - if centralIndex == nil { - if !storySubscriptions.items.isEmpty { - centralIndex = 0 + if centralIndex == nil { + if !storySubscriptions.items.isEmpty { + centralIndex = 0 + } + } + + if let centralIndex { + if storySubscriptions.items[centralIndex].hasUnseen { + startedWithUnseenValue = true + } } } - if let centralIndex { - if storySubscriptions.items[centralIndex].hasUnseen { - startedWithUnseenValue = true - } - } + self.startedWithUnseen = startedWithUnseenValue + startedWithUnseen = startedWithUnseenValue } - self.startedWithUnseen = startedWithUnseenValue - startedWithUnseen = startedWithUnseenValue - } - - var sortedItems: [EngineStorySubscriptions.Item] = [] - for peerId in self.fixedSubscriptionOrder { - if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) { - sortedItems.append(storySubscriptions.items[index]) + var sortedItems: [EngineStorySubscriptions.Item] = [] + for peerId in self.fixedSubscriptionOrder { + if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) { + sortedItems.append(storySubscriptions.items[index]) + } } - } - for item in storySubscriptions.items { - if !sortedItems.contains(where: { $0.peer.id == item.peer.id }) { - if startedWithUnseen { - if !item.hasUnseen { - continue + for item in storySubscriptions.items { + if !sortedItems.contains(where: { $0.peer.id == item.peer.id }) { + if startedWithUnseen { + if !item.hasUnseen { + continue + } + } + sortedItems.append(item) + } + } + self.fixedSubscriptionOrder = sortedItems.map(\.peer.id) + + self.storySubscriptions = EngineStorySubscriptions( + accountItem: storySubscriptions.accountItem, + items: sortedItems, + hasMoreToken: storySubscriptions.hasMoreToken + ) + self.updatePeerContexts() + }) + } else { + self.storySubscriptionsDisposable = (context.engine.messages.storySubscriptions(includeHidden: includeHidden) + |> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in + guard let self else { + return + } + + let startedWithUnseen: Bool + if let current = self.startedWithUnseen { + startedWithUnseen = current + } else { + var startedWithUnseenValue = false + + if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId { + } else { + var centralIndex: Int? + if let (focusedPeerId, _) = self.focusedItem { + if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) { + centralIndex = index + } + } + if centralIndex == nil { + if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { + centralIndex = index + } + } + if centralIndex == nil { + if !storySubscriptions.items.isEmpty { + centralIndex = 0 + } + } + + if let centralIndex { + if storySubscriptions.items[centralIndex].hasUnseen { + startedWithUnseenValue = true + } } } - sortedItems.append(item) + + self.startedWithUnseen = startedWithUnseenValue + startedWithUnseen = startedWithUnseenValue } - } - self.fixedSubscriptionOrder = sortedItems.map(\.peer.id) - - self.storySubscriptions = EngineStorySubscriptions( - accountItem: storySubscriptions.accountItem, - items: sortedItems, - hasMoreToken: storySubscriptions.hasMoreToken - ) - self.updatePeerContexts() - }) + + var sortedItems: [EngineStorySubscriptions.Item] = [] + for peerId in self.fixedSubscriptionOrder { + if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) { + sortedItems.append(storySubscriptions.items[index]) + } + } + for item in storySubscriptions.items { + if !sortedItems.contains(where: { $0.peer.id == item.peer.id }) { + if startedWithUnseen { + if !item.hasUnseen { + continue + } + } + sortedItems.append(item) + } + } + self.fixedSubscriptionOrder = sortedItems.map(\.peer.id) + + self.storySubscriptions = EngineStorySubscriptions( + accountItem: storySubscriptions.accountItem, + items: sortedItems, + hasMoreToken: storySubscriptions.hasMoreToken + ) + self.updatePeerContexts() + }) + } } deinit { @@ -415,6 +508,7 @@ public final class StoryContentContextImpl: StoryContentContext { disposable.dispose() } self.pollStoryMetadataDisposables.dispose() + self.storySubscriptionsDisposable?.dispose() } private func updatePeerContexts() { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 9618dd1fe0..f8cb4c7bdf 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -88,6 +88,8 @@ import AvatarEditorScreen import SendInviteLinkScreen import PeerInfoVisualMediaPaneNode import PeerInfoStoryGridScreen +import StoryContainerScreen +import StoryContentComponent enum PeerInfoAvatarEditingMode { case generic @@ -2146,6 +2148,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private var translationState: ChatTranslationState? private var translationStateDisposable: Disposable? + private var expiringStoryList: PeerExpiringStoryListContext? + private var expiringStoryListState: PeerExpiringStoryListContext.State? + private var expiringStoryListDisposable: Disposable? + private let _ready = Promise() var ready: Promise { return self._ready @@ -3038,6 +3044,58 @@ 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) + }) + + return + } + guard peer.smallProfileImage != nil else { return } @@ -3852,6 +3910,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> deliverOnMainQueue).start(next: { [weak self] translationState in self?.translationState = translationState }) + } 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 { + return + } + self.expiringStoryListState = state + }) } } @@ -3877,8 +3945,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.refreshMessageTagStatsDisposable?.dispose() self.forumTopicNotificationExceptionsDisposable?.dispose() self.translationStateDisposable?.dispose() - self.copyProtectionTooltipController?.dismiss() + self.expiringStoryListDisposable?.dispose() } override func didLoad() { From f8ecc999e67a42300fd55ab6ad64141876250e4d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 16:55:27 +0400 Subject: [PATCH 07/13] [WIP] Stories --- .../Messages/StoryListContext.swift | 22 +++++++++++++++++++ .../Sources/StoryChatContent.swift | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 70b0b01e1f..eec1979ba0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1007,6 +1007,24 @@ public final class PeerExpiringStoryListContext { public enum Item: Equatable { case item(EngineStoryItem) case placeholder(id: Int32, timestamp: Int32, expirationTimestamp: Int32) + + public var id: Int32 { + switch self { + case let .item(item): + return item.id + case let .placeholder(id, _, _): + return id + } + } + + public var timestamp: Int32 { + switch self { + case let .item(item): + return item.timestamp + case let .placeholder(_, timestamp, _): + return timestamp + } + } } public final class State: Equatable { @@ -1014,6 +1032,10 @@ public final class PeerExpiringStoryListContext { public let isCached: Bool public let maxReadId: Int32 + public var hasUnseen: Bool { + return self.items.contains(where: { $0.id > self.maxReadId }) + } + public init(items: [Item], isCached: Bool, maxReadId: Int32) { self.items = items self.isCached = isCached diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index a7f5954fdd..d3cbbc93b1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -350,7 +350,7 @@ public final class StoryContentContextImpl: StoryContentContext { let singlePeerListContext = PeerExpiringStoryListContext(account: context.account, peerId: focusedPeerId) self.singlePeerListContext = singlePeerListContext self.storySubscriptionsDisposable = (combineLatest( - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: focusedPeerId)) + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: focusedPeerId)), singlePeerListContext.state ) |> deliverOnMainQueue).start(next: { [weak self] peer, state in From a114fe89f726ce36476e7b3058a5d6c244d27530 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Fri, 9 Jun 2023 16:59:16 +0400 Subject: [PATCH 08/13] hasUnseen for peer state --- .../Messages/StoryListContext.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 70b0b01e1f..a1894173af 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1032,6 +1032,22 @@ public final class PeerExpiringStoryListContext { } return true } + + public var hasUnseen: Bool { + for item in items { + switch item { + case let .item(item): + if item.id > maxReadId { + return true + } + case let .placeholder(id, _, _): + if id > maxReadId { + return true + } + } + } + return false + } } private let queue: Queue From 225b46d75d09032c8e2dc600f165b3638bbf5e5c Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Fri, 9 Jun 2023 18:32:54 +0400 Subject: [PATCH 09/13] macos syntax --- .../TelegramEngine/Messages/StoryListContext.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index a1894173af..104b45a80a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -485,7 +485,7 @@ public final class PeerStoryListContext { return (peerReference, items, Int(cached.totalCount)) } |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount in - guard let self else { + guard let `self` = self else { return } @@ -853,7 +853,7 @@ public final class PeerExpiringStoryListContext { PostboxViewKey.storyItems(peerId: peerId) ]) |> deliverOn(self.queue)).start(next: { [weak self] views in - guard let self else { + guard let `self` = self else { return } guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else { @@ -907,10 +907,10 @@ public final class PeerExpiringStoryListContext { ) } |> deliverOn(self.queue)).start(next: { [weak self] state in - guard let self else { + guard let `self` = self else { return } - guard let state else { + guard let state = state else { return } self.statePromise.set(.single(state)) @@ -934,7 +934,7 @@ public final class PeerExpiringStoryListContext { return transaction.getPeer(peerId).flatMap(apiInputUser) } |> mapToSignal { inputUser -> Signal in - guard let inputUser else { + guard let inputUser = inputUser else { return .complete() } return account.network.request(Api.functions.stories.getUserStories(userId: inputUser)) From 434e9dd22d88f767e8f266e0b7ba939d27114282 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 20:17:50 +0400 Subject: [PATCH 10/13] Story improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/ChatListController.swift | 8 +- .../Sources/ChatListControllerNode.swift | 23 ++++-- .../Sources/Node/ChatListItemStrings.swift | 3 + .../Sources/Node/ChatListNode.swift | 8 +- submodules/Postbox/Sources/Media.swift | 7 ++ .../Postbox/Sources/MessageHistoryTable.swift | 10 ++- .../Account/AccountIntermediateState.swift | 19 ++++- .../Sources/Account/AccountManager.swift | 1 + .../ApiUtils/StoreMessage_Telegram.swift | 4 +- .../PendingMessageUploadedContent.swift | 9 +++ .../State/AccountStateManagementUtils.swift | 64 ++++++++++++--- .../Sources/State/AccountStateManager.swift | 3 +- .../TelegramCore/Sources/State/Holes.swift | 14 +++- .../SyncCore_TelegramMediaStory.swift | 54 +++++++++++++ .../TelegramEngine/Messages/Media.swift | 7 ++ .../TelegramCore/Sources/UpdatePeers.swift | 2 +- submodules/TelegramUI/BUILD | 1 + .../AvatarStoryIndicatorComponent/BUILD | 20 +++++ .../AvatarStoryIndicatorComponent.swift | 81 +++++++++++++++++++ ...StoryItemSetContainerViewSendMessage.swift | 30 +++---- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 35 +++++++- .../Sources/ChatMessageForwardInfoNode.swift | 11 ++- .../ChatMessageInstantVideoItemNode.swift | 2 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 33 ++++++++ .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 39 +++++++++ .../Sources/PeerInfo/PeerInfoScreen.swift | 6 ++ .../Sources/PeerSelectionControllerNode.swift | 2 +- 31 files changed, 443 insertions(+), 61 deletions(-) create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaStory.swift create mode 100644 submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c5773b83cd..e65b718ce0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9357,3 +9357,5 @@ Sorry for the inconvenience."; "Privacy.Bio.NeverShareWith.Title" = "Never Share With"; "Conversation.OpenLink" = "OPEN LINK"; + +"Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@"; \ No newline at end of file diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6fd78b7dc6..47da8c01b2 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -316,6 +316,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } + + if "".isEmpty { + strongSelf.scrollToStories() + return + } + if strongSelf.chatListDisplayNode.searchDisplayController != nil { strongSelf.deactivateSearch(animated: true) } else { @@ -2494,7 +2500,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } public func scrollToStories() { - + self.chatListDisplayNode.scrollToStories() } private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index bb45f15011..40c8c706f2 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -773,6 +773,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele private var selectedId: ChatListFilterTabEntryId var storiesUnlocked: Bool = false + var ignoreStoryUnlockedScrolling: Bool = false var initialScrollingOffset: CGFloat? @@ -936,7 +937,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } else if self.storiesUnlocked { switch offset { case let .known(value): - if value >= ChatListNavigationBar.storiesScrollHeight { + if value >= ChatListNavigationBar.storiesScrollHeight && !self.ignoreStoryUnlockedScrolling { self.storiesUnlocked = false self.onStoriesLockedUpdated?(false) } @@ -1326,9 +1327,9 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } } - public func scrollToTop() { + public func scrollToTop(animated: Bool) { if let itemNode = self.itemNodes[self.selectedId] { - itemNode.listNode.scrollToPosition(.top) + itemNode.listNode.scrollToPosition(.top, animated: animated) } } @@ -1965,6 +1966,18 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { let _ = self.updateNavigationBar(layout: layout, transition: transition) } + func scrollToStories() { + if self.inlineStackContainerNode != nil { + return + } + self.mainContainerNode.scrollToTop(animated: false) + self.mainContainerNode.storiesUnlocked = true + self.mainContainerNode.ignoreStoryUnlockedScrolling = true + self.controller?.requestLayout(transition: .immediate) + self.mainContainerNode.scrollToTop(animated: false) + self.mainContainerNode.ignoreStoryUnlockedScrolling = false + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { var navigationBarHeight = navigationBarHeight var visualNavigationHeight = visualNavigationHeight @@ -2402,9 +2415,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { if let searchDisplayController = self.searchDisplayController { searchDisplayController.contentNode.scrollToTop() } else if let inlineStackContainerNode = self.inlineStackContainerNode { - inlineStackContainerNode.scrollToTop() + inlineStackContainerNode.scrollToTop(animated: true) } else { - self.mainContainerNode.scrollToTop() + self.mainContainerNode.scrollToTop(animated: true) } } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index d1305205f3..8ab977b34c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -295,6 +295,9 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: messageText = "📊 \(poll.text)" case let dice as TelegramMediaDice: messageText = dice.emoji + case _ as TelegramMediaStory: + //TODO:localize + messageText = "Story" default: break } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index dec2f36c78..54e6ef2d50 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -3309,16 +3309,16 @@ public final class ChatListNode: ListView { } } - public func scrollToPosition(_ position: ChatListNodeScrollPosition) { + public func scrollToPosition(_ position: ChatListNodeScrollPosition, animated: Bool = true) { if let list = self.chatListView?.originalList { if !list.hasLater { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { - let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter) + let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } else { - let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter) + let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 70d6f75241..066afb81ec 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -71,6 +71,7 @@ public struct MediaId: Hashable, PostboxCoding, CustomStringConvertible, Codable public protocol Media: AnyObject, PostboxCoding { var id: MediaId? { get } var peerIds: [PeerId] { get } + var storyIds: [StoryId] { get } var indexableText: String? { get } @@ -82,6 +83,12 @@ public protocol Media: AnyObject, PostboxCoding { func isSemanticallyEqual(to other: Media) -> Bool } +public extension Media { + var storyIds: [StoryId] { + return [] + } +} + public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { if lhs.count != rhs.count { return false diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index db94b265bf..47e00a3f75 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -2574,18 +2574,26 @@ final class MessageHistoryTable: Table { } } + var associatedStories: [StoryId: CodableEntry] = [:] + for media in parsedMedia { for peerId in media.peerIds { if let peer = peerTable.get(peerId) { peers[peer.id] = peer } } + for storyId in media.storyIds { + if associatedStories[storyId] == nil { + if let story = storyTable.get(id: storyId) { + associatedStories[storyId] = story + } + } + } } var associatedMessageIds: [MessageId] = [] var associatedMessages = SimpleDictionary() var associatedMedia: [MediaId: Media] = [:] - var associatedStories: [StoryId: CodableEntry] = [:] for attribute in parsedAttributes { for peerId in attribute.associatedPeerIds { diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 14430a5ca5..defb850e8e 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -177,6 +177,12 @@ struct ReferencedReplyMessageIds { } } +enum UpdatesStoredStory { + case item(Stories.Item) + case placeholder(Stories.Placeholder) + case deleted +} + struct AccountMutableState { let initialState: AccountInitialState let branchOperationIndex: Int @@ -195,6 +201,7 @@ struct AccountMutableState { var readInboxMaxIds: [PeerId: MessageId] var namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]] var updatedOutgoingUniqueMessageIds: [Int64: Int32] + var storedStories: [StoryId: UpdatesStoredStory] var resetForumTopicLists: [PeerId: StateResetForumTopics] = [:] @@ -213,7 +220,7 @@ struct AccountMutableState { var authorizationListUpdated: Bool = false - init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { + init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { self.initialState = initialState self.state = initialState.state self.peers = initialPeers @@ -221,6 +228,7 @@ struct AccountMutableState { self.referencedReplyMessageIds = initialReferencedReplyMessageIds self.referencedGeneralMessageIds = initialReferencedGeneralMessageIds self.storedMessages = initialStoredMessages + self.storedStories = initialStoredStories self.readInboxMaxIds = initialReadInboxMaxIds self.channelStates = initialState.channelStates self.peerChatInfos = initialState.peerChatInfos @@ -230,7 +238,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -240,6 +248,7 @@ struct AccountMutableState { self.referencedReplyMessageIds = referencedReplyMessageIds self.referencedGeneralMessageIds = referencedGeneralMessageIds self.storedMessages = storedMessages + self.storedStories = storedStories self.peerChatInfos = peerChatInfos self.readInboxMaxIds = readInboxMaxIds self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp @@ -251,13 +260,17 @@ struct AccountMutableState { } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { self.referencedReplyMessageIds.formUnion(other.referencedReplyMessageIds) self.referencedGeneralMessageIds.formUnion(other.referencedGeneralMessageIds) + for (id, story) in other.storedStories { + self.storedStories[id] = story + } + for i in other.branchOperationIndex ..< other.operations.count { self.addOperation(other.operations[i]) } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index fe33725896..725b68f8a5 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -195,6 +195,7 @@ private var declaredEncodables: Void = { declareEncodable(MediaSpoilerMessageAttribute.self, f: { MediaSpoilerMessageAttribute(decoder: $0) }) declareEncodable(TranslationMessageAttribute.self, f: { TranslationMessageAttribute(decoder: $0) }) declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) }) + declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 526c96d843..aac9489a44 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -381,9 +381,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaDice(value, emoticon): return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil) case let .messageMediaStory(userId, id): - let _ = userId - let _ = id - return (nil, nil, nil, nil) + return (TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id)), nil, nil, nil) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index e19bdc202b..7dd47bf835 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -87,6 +87,15 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)), .text) } else if let contextResult = contextResult { return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil, cacheReferenceKey: nil)), .text) + } else if let media = media.first as? TelegramMediaStory { + //Signal + return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in + guard let inputUser = transaction.getPeer(media.storyId.peerId).flatMap(apiInputUser) else { + return .progress(0.0) + } + return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) + } + |> castError(PendingMessageUploadError.self), .text) } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { return .signal(mediaResult, .media) } else { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a9e084c183..fe503dcd3d 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -203,6 +203,20 @@ private func locallyGeneratedMessageTimestampsFromUpdateGroups(_ groups: [Update return messageTimestamps } +private func associatedStoredStories(_ groups: [UpdateGroup]) -> [StoryId: UpdatesStoredStory] { + var storedStories: [StoryId: UpdatesStoredStory] = [:] + storedStories.removeAll() + + return storedStories +} + +private func associatedStoredStories(_ difference: Api.updates.Difference) -> [StoryId: UpdatesStoredStory] { + var storedStories: [StoryId: UpdatesStoredStory] = [:] + storedStories.removeAll() + + return storedStories +} + private func peerIdsFromDifference(_ difference: Api.updates.Difference) -> Set { var peerIds = Set() @@ -421,7 +435,7 @@ private func locallyGeneratedMessageTimestampsFromDifference(_ difference: Api.u return messageTimestamps } -func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, activeChannelIds: Set, referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, peerIdsRequiringLocalChatState: Set, locallyGeneratedMessageTimestamps: [PeerId: [(MessageId.Namespace, Int32)]]) -> AccountMutableState { +func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, activeChannelIds: Set, referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, peerIdsRequiringLocalChatState: Set, locallyGeneratedMessageTimestamps: [PeerId: [(MessageId.Namespace, Int32)]], storedStories: [StoryId: UpdatesStoredStory]) -> AccountMutableState { var peers: [PeerId: Peer] = [:] var channelStates: [PeerId: AccountStateChannelState] = [:] @@ -515,7 +529,7 @@ func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, a } } - let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp) + let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp) return state } @@ -526,7 +540,7 @@ func initialStateWithUpdateGroups(postbox: Postbox, groups: [UpdateGroup]) -> Si let associatedMessageIds = associatedMessageIdsFromUpdateGroups(groups) let peerIdsRequiringLocalChatState = peerIdsRequiringLocalChatStateFromUpdateGroups(groups) - return initialStateWithPeerIds(transaction, peerIds: peerIds, activeChannelIds: activeChannelIds, referencedReplyMessageIds: associatedMessageIds.replyIds, referencedGeneralMessageIds: associatedMessageIds.generalIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestampsFromUpdateGroups(groups)) + return initialStateWithPeerIds(transaction, peerIds: peerIds, activeChannelIds: activeChannelIds, referencedReplyMessageIds: associatedMessageIds.replyIds, referencedGeneralMessageIds: associatedMessageIds.generalIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestampsFromUpdateGroups(groups), storedStories: associatedStoredStories(groups)) } } @@ -536,7 +550,7 @@ func initialStateWithDifference(postbox: Postbox, difference: Api.updates.Differ let activeChannelIds = activeChannelsFromDifference(difference) let associatedMessageIds = associatedMessageIdsFromDifference(difference) let peerIdsRequiringLocalChatState = peerIdsRequiringLocalChatStateFromDifference(difference) - return initialStateWithPeerIds(transaction, peerIds: peerIds, activeChannelIds: activeChannelIds, referencedReplyMessageIds: associatedMessageIds.replyIds, referencedGeneralMessageIds: associatedMessageIds.generalIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestampsFromDifference(difference)) + return initialStateWithPeerIds(transaction, peerIds: peerIds, activeChannelIds: activeChannelIds, referencedReplyMessageIds: associatedMessageIds.replyIds, referencedGeneralMessageIds: associatedMessageIds.generalIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestampsFromDifference(difference), storedStories: associatedStoredStories(difference)) } } @@ -1701,9 +1715,12 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: |> mapToSignal { finalState in return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in - return resolveMissingPeerChatInfos(network: network, state: resultingState) - |> map { resultingState, resolveError -> AccountFinalState in - return AccountFinalState(state: resultingState, shouldPoll: shouldPoll || hadError || resolveError, incomplete: missingUpdates, missingUpdatesFromChannels: Set(), discard: resolveError) + return resolveAssociatedStories(postbox: postbox, network: network, state: finalState) + |> mapToSignal { resultingState -> Signal in + return resolveMissingPeerChatInfos(network: network, state: resultingState) + |> map { resultingState, resolveError -> AccountFinalState in + return AccountFinalState(state: resultingState, shouldPoll: shouldPoll || hadError || resolveError, incomplete: missingUpdates, missingUpdatesFromChannels: Set(), discard: resolveError) + } } } } @@ -2056,6 +2073,20 @@ func resolveForumThreads(postbox: Postbox, network: Network, fetchedChatList: Fe } } +func resolveAssociatedStories(postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { + return postbox.transaction { transaction -> Signal in + return .single(state) + } + |> switchToLatest +} + +func resolveAssociatedStories(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], result: T) -> Signal { + return postbox.transaction { transaction -> Signal in + return .single(result) + } + |> switchToLatest +} + func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set) { for attribute in message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { @@ -2384,10 +2415,13 @@ func pollChannelOnce(accountPeerId: PeerId, postbox: Postbox, network: Network, peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) + |> mapToSignal { resultingState -> Signal in + return resolveAssociatedStories(postbox: postbox, network: network, state: finalState) + } |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in @@ -2438,10 +2472,13 @@ public func standalonePollChannelOnce(accountPeerId: PeerId, postbox: Postbox, n peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) + |> mapToSignal { resultingState -> Signal in + return resolveAssociatedStories(postbox: postbox, network: network, state: finalState) + } |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in @@ -2650,7 +2687,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe // TODO: delete messages later than top return resolveAssociatedMessages(postbox: postbox, network: network, state: updatedState) |> mapToSignal { resultingState -> Signal in - return .single(resultingState) + return resolveAssociatedStories(postbox: postbox, network: network, state: updatedState) } } } @@ -2799,8 +2836,11 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo } return resolveForumThreads(postbox: postbox, network: network, state: updatedState) - |> map { updatedState -> (AccountMutableState, Bool, Int32?) in - return (updatedState, true, apiTimeout) + |> mapToSignal { updatedState in + return resolveAssociatedStories(postbox: postbox, network: network, state: updatedState) + |> map { updatedState -> (AccountMutableState, Bool, Int32?) in + return (updatedState, true, apiTimeout) + } } case let .channelDifferenceEmpty(_, pts, timeout): var updatedState = state diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1a27c50fb9..9faf4f49f7 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -526,7 +526,8 @@ public final class AccountStateManager { referencedReplyMessageIds: ReferencedReplyMessageIds(), referencedGeneralMessageIds: Set(), peerIdsRequiringLocalChatState: Set(), - locallyGeneratedMessageTimestamps: [:] + locallyGeneratedMessageTimestamps: [:], + storedStories: [:] ) } |> mapToSignal { state -> Signal in diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 6e8f7e1239..169a101bc5 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -129,8 +129,11 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis if referencedReplyIds.isEmpty && referencedGeneralIds.isEmpty { return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages, reactions: [], result: Void()) |> mapToSignal { _ -> Signal in - return postbox.transaction { transaction -> T in - return f(transaction, [], []) + return resolveAssociatedStories(postbox: postbox, source: source, messages: storeMessages, result: Void()) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> T in + return f(transaction, [], []) + } } } } else { @@ -224,8 +227,11 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages + additionalMessages, reactions: [], result: Void()) |> mapToSignal { _ -> Signal in - return postbox.transaction { transaction -> T in - return f(transaction, additionalPeers, additionalMessages) + return resolveAssociatedStories(postbox: postbox, source: source, messages: storeMessages + additionalMessages, result: Void()) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> T in + return f(transaction, additionalPeers, additionalMessages) + } } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaStory.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaStory.swift new file mode 100644 index 0000000000..efe709bb70 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaStory.swift @@ -0,0 +1,54 @@ +import Postbox + +public final class TelegramMediaStory: Media, Equatable { + public var id: MediaId? { + return nil + } + public let peerIds: [PeerId] + + public let storyId: StoryId + + public var storyIds: [StoryId] { + return [self.storyId] + } + + public init(storyId: StoryId) { + self.storyId = storyId + + self.peerIds = [self.storyId.peerId] + } + + public init(decoder: PostboxDecoder) { + self.storyId = StoryId(peerId: PeerId(decoder.decodeInt64ForKey("pid", orElse: 0)), id: decoder.decodeInt32ForKey("sid", orElse: 0)) + + self.peerIds = [self.storyId.peerId] + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.storyId.peerId.toInt64(), forKey: "pid") + encoder.encodeInt32(self.storyId.id, forKey: "sid") + } + + public func isLikelyToBeUpdated() -> Bool { + return false + } + + public func isEqual(to other: Media) -> Bool { + if let other = other as? TelegramMediaStory, self.storyId == other.storyId { + return self == other + } + return false + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } + + public static func ==(lhs: TelegramMediaStory, rhs: TelegramMediaStory) -> Bool { + if lhs.storyId != rhs.storyId { + return false + } + + return true + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift index a590cc269c..639a0eb790 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift @@ -16,6 +16,7 @@ public enum EngineMedia: Equatable { case unsupported(TelegramMediaUnsupported) case webFile(TelegramMediaWebFile) case webpage(TelegramMediaWebpage) + case story(TelegramMediaStory) } public extension EngineMedia { @@ -47,6 +48,8 @@ public extension EngineMedia { return webFile.id case let .webpage(webpage): return webpage.id + case let .story(story): + return story.id } } } @@ -80,6 +83,8 @@ public extension EngineMedia { self = .webFile(webFile) case let webpage as TelegramMediaWebpage: self = .webpage(webpage) + case let story as TelegramMediaStory: + self = .story(story) default: preconditionFailure() } @@ -113,6 +118,8 @@ public extension EngineMedia { return webFile case let .webpage(webpage): return webpage + case let .story(story): + return story } } } diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index c694deb0d8..c5059b4000 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -53,7 +53,7 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?, switch peerId.namespace { case Namespaces.Peer.CloudUser: - if let updated = updated as? TelegramUser, let storiesHidden = updated.storiesHidden { + if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser, let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden { if storiesHidden { if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 7441d5e1b9..81f1becf9e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -373,6 +373,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/BUILD b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/BUILD new file mode 100644 index 0000000000..0a8d337e78 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AvatarStoryIndicatorComponent", + module_name = "AvatarStoryIndicatorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift new file mode 100644 index 0000000000..b7b71bc3bf --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -0,0 +1,81 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData + +public final class AvatarStoryIndicatorComponent: Component { + public let hasUnseen: Bool + + public init( + hasUnseen: Bool + ) { + self.hasUnseen = hasUnseen + } + + public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { + if lhs.hasUnseen != rhs.hasUnseen { + return false + } + return true + } + + public final class View: UIView { + private let indicatorView: UIImageView + + private var component: AvatarStoryIndicatorComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.indicatorView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.indicatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarStoryIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let lineWidth: CGFloat = 3.0 + + self.indicatorView.image = generateImage(CGSize(width: availableSize.width + lineWidth * 2.0, height: availableSize.width + lineWidth * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(lineWidth) + context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.replacePathWithStrokedPath() + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] + if component.hasUnseen { + colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(), size: availableSize).insetBy(dx: -lineWidth * 2.0, dy: -lineWidth * 2.0)) + + return availableSize + } + } + + 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/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index edbf991622..2fae187314 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -276,6 +276,9 @@ final class StoryItemSetContainerSendMessage { guard let component = view.component else { return } + guard let controller = component.controller() else { + return + } let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { return @@ -285,21 +288,18 @@ final class StoryItemSetContainerSendMessage { case .like: return case .share: - let _ = (component.context.engine.messages.exportStoryLink(peerId: peerId, id: focusedItem.storyItem.id) - |> deliverOnMainQueue).start(next: { [weak view] link in - guard let view, let link, let component = view.component, let controller = component.controller() else { - return - } - let shareController = ShareController( - context: component.context, - subject: .url(link), - externalShare: false, - immediateExternalShare: false, - updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }), - component.context.sharedContext.presentationData) - ) - controller.present(shareController, in: .window(.root)) - }) + /*let linkPromise = Promise() + linkPromise.set(component.context.engine.messages.exportStoryLink(peerId: peerId, id: focusedItem.storyItem.id))*/ + + let shareController = ShareController( + context: component.context, + subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id)))), + externalShare: false, + immediateExternalShare: false, + updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }), + component.context.sharedContext.presentationData) + ) + controller.present(shareController, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 7830feb4dd..cc904d33eb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1348,7 +1348,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } let availableWidth = max(60.0, availableContentWidth + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, false, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index d4c8c17311..933594207b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -127,6 +127,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageWithCaptionToAdd = (message, itemAttributes) } result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) + } else if let _ = media as? TelegramMediaStory { + if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty { + messageWithCaptionToAdd = (message, itemAttributes) + } + result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else if let file = media as? TelegramMediaFile { let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil) if isVideo { @@ -1121,7 +1126,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), - forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), + forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, Bool, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), @@ -1706,7 +1711,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else if let backgroundHiding, case .always = backgroundHiding { initialDisplayHeader = false } else { - if inlineBotNameString == nil && (ignoreForward || firstMessage.forwardInfo == nil) && replyMessage == nil && replyStory == nil { + var hasForwardLikeContent = false + if firstMessage.forwardInfo != nil { + hasForwardLikeContent = true + } else if firstMessage.media.contains(where: { $0 is TelegramMediaStory }) { + hasForwardLikeContent = true + } + + if inlineBotNameString == nil && (ignoreForward || !hasForwardLikeContent) && replyMessage == nil && replyStory == nil { if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader && !ignoreNameHiding { if let author = firstMessage.author as? TelegramChannel, case .group = author.info, author.id == firstMessage.id.peerId, !incoming { } else { @@ -1773,6 +1785,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if firstMessage.forwardInfo != nil { displayHeader = true } + if firstMessage.media.contains(where: { $0 is TelegramMediaStory }) { + displayHeader = true + } if replyMessage != nil || replyStory != nil { displayHeader = true } @@ -1997,7 +2012,21 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardAuthorSignature = forwardInfo.authorSignature } } - let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, false, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) + + forwardInfoOriginY = headerSize.height + headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + bubbleWidthInsets) + headerSize.height += forwardInfoSizeApply.0.height + } else if let storyMedia = firstMessage.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory { + let _ = storyMedia + if headerSize.height.isZero { + headerSize.height += 5.0 + } + + forwardSource = firstMessage.peers[storyMedia.storyId.peerId] + + let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, nil, nil, true, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) forwardInfoOriginY = headerSize.height diff --git a/submodules/TelegramUI/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageForwardInfoNode.swift index 3a918999b5..fb8d39d8b5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageForwardInfoNode.swift @@ -91,10 +91,10 @@ class ChatMessageForwardInfoNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ isStory: Bool, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) - return { presentationData, strings, type, peer, authorName, psaType, constrainedSize in + return { presentationData, strings, type, peer, authorName, psaType, isStory, constrainedSize in let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0) let prefixFont = Font.regular(fontSize) let peerFont = Font.medium(fontSize) @@ -148,7 +148,12 @@ class ChatMessageForwardInfoNode: ASDisplayNode { } } else { titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor - completeSourceString = strings.Message_ForwardedMessageShort(peerString) + + if isStory { + completeSourceString = strings.Message_ForwardedStoryShort(peerString) + } else { + completeSourceString = strings.Message_ForwardedMessageShort(peerString) + } } case .standalone: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 95f8473a86..93de93541a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -528,7 +528,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } let availableWidth = max(60.0, availableContentWidth - normalDisplaySize.width + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, false, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index b01603406a..88280f5780 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -399,7 +399,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } let availableWidth = max(60.0, availableContentWidth - 210.0 + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, false, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } var notConsumed = false diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 4bca1c9c06..ac979bf2a4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -91,6 +91,39 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { automaticDownload = .full } + } else if let telegramStory = media as? TelegramMediaStory { + if let storyMedia = item.message.associatedStories[telegramStory.storyId], case let .item(storyItem) = storyMedia.get(Stories.StoredItem.self), let media = storyItem.media { + if let telegramImage = media as? TelegramMediaImage { + selectedMedia = telegramImage + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { + automaticDownload = .full + } + } else if let telegramFile = media as? TelegramMediaFile { + selectedMedia = telegramFile + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { + automaticDownload = .full + } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { + automaticDownload = .prefetch + } + + if !item.message.containsSecretMedia { + if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + } + } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + } + } + } + contentMode = .aspectFill + } + } } else if let telegramFile = media as? TelegramMediaFile { selectedMedia = telegramFile if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index c860cc746e..da73561f62 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -744,7 +744,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } let availableForwardWidth = max(60.0, availableWidth + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, false, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) } var needsReplyBackground = false diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index c0af3f13b3..65b57d9a9d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -31,6 +31,8 @@ import ChatTitleView import AppBundle import AvatarVideoNode import PeerInfoVisualMediaPaneNode +import AvatarStoryIndicatorComponent +import ComponentDisplayAdapters enum PeerInfoHeaderButtonKey: Hashable { case message @@ -393,6 +395,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let containerNode: ContextControllerSourceNode let avatarNode: AvatarNode + private(set) var avatarStoryView: ComponentView? fileprivate var videoNode: UniversalVideoNode? fileprivate var markupNode: AvatarVideoNode? fileprivate var iconView: ComponentView? @@ -417,6 +420,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { private let playbackStartDisposable = MetaDisposable() + var hasUnseenStories: Bool? + init(context: AccountContext) { self.context = context self.containerNode = ContextControllerSourceNode() @@ -433,6 +438,8 @@ 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 { @@ -448,6 +455,38 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.playbackStartDisposable.dispose() } + func updateStoryView(transition: ContainedViewLayoutTransition) { + if let hasUnseenStories = self.hasUnseenStories { + let avatarStoryView: ComponentView + if let current = self.avatarStoryView { + avatarStoryView = current + } else { + avatarStoryView = ComponentView() + self.avatarStoryView = avatarStoryView + } + + let _ = avatarStoryView.update( + transition: Transition(transition), + component: AnyComponent(AvatarStoryIndicatorComponent( + hasUnseen: hasUnseenStories + )), + environment: {}, + containerSize: self.avatarNode.bounds.size + ) + if let avatarStoryComponentView = avatarStoryView.view { + if avatarStoryComponentView.superview == nil { + self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0) + } + avatarStoryComponentView.frame = self.avatarNode.frame + } + } else { + if let avatarStoryView = self.avatarStoryView { + self.avatarStoryView = nil + avatarStoryView.view?.removeFromSuperview() + } + } + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.tapped?() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f8cb4c7bdf..a9a0dee67a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3919,6 +3919,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.expiringStoryListState = state + if state.items.isEmpty { + self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = nil + } else { + self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = state.hasUnseen + } + self.headerNode.avatarListNode.avatarContainerNode.updateStoryView(transition: .immediate) }) } } diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 9ec9474264..83c264a2fd 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -1291,7 +1291,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { func scrollToTop() { if self.mainContainerNode?.supernode != nil { - self.mainContainerNode?.scrollToTop() + self.mainContainerNode?.scrollToTop(animated: true) } else if self.chatListNode?.supernode != nil { self.chatListNode?.scrollToPosition(.top) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { From 0252319cd1c429cb403d2d558e9a96a9ae58b903 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 20:18:34 +0400 Subject: [PATCH 11/13] Remove duplicate --- .../Messages/StoryListContext.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 40123e5a12..25e0784e91 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1054,22 +1054,6 @@ public final class PeerExpiringStoryListContext { } return true } - - public var hasUnseen: Bool { - for item in items { - switch item { - case let .item(item): - if item.id > maxReadId { - return true - } - case let .placeholder(id, _, _): - if id > maxReadId { - return true - } - } - } - return false - } } private let queue: Queue From 749023af1f4b4b25530ed2f1fb97520fd76c253b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Jun 2023 21:12:49 +0400 Subject: [PATCH 12/13] Remove debugging --- submodules/ChatListUI/Sources/ChatListController.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 47da8c01b2..ec5f16b7f4 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -317,11 +317,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - if "".isEmpty { - strongSelf.scrollToStories() - return - } - if strongSelf.chatListDisplayNode.searchDisplayController != nil { strongSelf.deactivateSearch(animated: true) } else { From 8a09cd5f70af3bc3f48d42d064ea7d4110be0bd8 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 10 Jun 2023 00:29:41 +0400 Subject: [PATCH 13/13] [WIP] Stories --- .../Postbox/Sources/MessageHistoryView.swift | 12 ++ .../Sources/MessageHistoryViewState.swift | 57 ++++++ .../Sources/State/AccountViewTracker.swift | 66 +++++++ .../TelegramEngine/Messages/Stories.swift | 33 ++++ .../Messages/TelegramEngineMessages.swift | 18 +- .../AvatarStoryIndicatorComponent.swift | 18 +- .../TelegramUI/Sources/ChatHistoryEntry.swift | 28 +++ .../Sources/ChatHistoryListNode.swift | 16 ++ .../ChatMessageInteractiveMediaNode.swift | 174 +++++++++++++++++- .../ChatMessageMediaBubbleContentNode.swift | 3 +- .../TelegramUI/Sources/OpenChatMessage.swift | 27 +++ .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 5 + 12 files changed, 433 insertions(+), 24 deletions(-) diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 97995bad9a..9b2abf30a7 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -666,6 +666,18 @@ final class MutableMessageHistoryView { hasChanges = true } } + if !transaction.storyEvents.isEmpty { + var updatedStories = Set() + for event in transaction.storyEvents { + switch event { + case let .updated(id): + updatedStories.insert(id) + } + } + if loadedState.updateStories(postbox: postbox, updatedStories: updatedStories) { + hasChanges = true + } + } } if hasChanges { diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index 68a8703e82..cec31265a6 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -1457,6 +1457,63 @@ final class HistoryViewLoadedState { return updated } + func updateStories(postbox: PostboxImpl, updatedStories: Set) -> Bool { + var updated = false + for space in self.orderedEntriesBySpace.keys { + let spaceUpdated = self.orderedEntriesBySpace[space]!.mutableScan({ entry in + switch entry { + case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers): + let message = value.message + var rebuild = false + for media in message.media { + for storyId in media.storyIds { + if updatedStories.contains(storyId) { + rebuild = true + break + } + } + } + for attribute in message.attributes { + for storyId in attribute.associatedStoryIds { + if updatedStories.contains(storyId) { + rebuild = true + break + } + } + } + + if rebuild { + let messageMedia: [Media] = message.media + var associatedStories: [StoryId: CodableEntry] = [:] + for media in message.media { + for storyId in media.storyIds { + if associatedStories[storyId] == nil, let value = postbox.storyTable.get(id: storyId) { + associatedStories[storyId] = value + } + } + } + for attribute in message.attributes { + for storyId in attribute.associatedStoryIds { + if associatedStories[storyId] == nil, let value = postbox.storyTable.get(id: storyId) { + associatedStories[storyId] = value + } + } + } + let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, threadId: message.threadId, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: messageMedia, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds, associatedMedia: message.associatedMedia, associatedThreadInfo: message.associatedThreadInfo, associatedStories: associatedStories) + return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) + } + case .IntermediateMessageEntry: + break + } + return nil + }) + if spaceUpdated { + updated = true + } + } + return updated + } + func add(entry: MutableMessageHistoryEntry) -> Bool { if let ignoreMessagesInTimestampRange = self.ignoreMessagesInTimestampRange { if ignoreMessagesInTimestampRange.contains(entry.index.timestamp) { diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 137ad7238c..baf12fa36c 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -308,6 +308,7 @@ public final class AccountViewTracker { private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var refreshSecretChatMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] + private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0 private var updatedUnsupportedMediaDisposables = DisposableDict() @@ -1229,6 +1230,71 @@ public final class AccountViewTracker { } } + public func refreshStoriesForMessageIds(messageIds: Set) { + self.queue.async { + var addedMessageIds: [MessageId] = [] + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + for messageId in messageIds { + let messageTimestamp = self.refreshStoriesForMessageIdsAndTimestamps[messageId] + if messageTimestamp == nil { + self.refreshStoriesForMessageIdsAndTimestamps[messageId] = timestamp + addedMessageIds.append(messageId) + } + } + if !addedMessageIds.isEmpty { + for (_, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + let disposableId = self.nextUpdatedUnsupportedMediaDisposableId + self.nextUpdatedUnsupportedMediaDisposableId += 1 + + if let account = self.account { + let signal = account.postbox.transaction { transaction -> Set in + var result = Set() + for id in messageIds { + if let message = transaction.getMessage(id) { + for media in message.media { + if let storyMedia = media as? TelegramMediaStory { + result.insert(storyMedia.storyId) + } + } + } + } + return result + } + |> mapToSignal { ids -> Signal in + guard !ids.isEmpty else { + return .complete() + } + + var requests: [Signal] = [] + + var idsGroupedByPeerId: [PeerId: Set] = [:] + for id in ids { + if idsGroupedByPeerId[id.peerId] == nil { + idsGroupedByPeerId[id.peerId] = Set([id.id]) + } else { + idsGroupedByPeerId[id.peerId]?.insert(id.id) + } + } + + for (peerId, ids) in idsGroupedByPeerId { + requests.append(_internal_refreshStories(account: account, peerId: peerId, ids: Array(ids))) + } + + return combineLatest(requests) + |> ignoreValues + } + |> afterDisposed { [weak self] in + self?.queue.async { + self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId) + } + } + self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId) + } + } + } + } + } + public func updateMarkAllMentionsSeen(peerId: PeerId, threadId: Int64?) { self.queue.async { guard let account = self.account else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 50c57f475b..ff352c23e5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1491,3 +1491,36 @@ func _internal_exportStoryLink(account: Account, peerId: EnginePeer.Id, id: Int3 } } } + +func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) -> Signal { + return _internal_getStoriesById(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, peerId: peerId, ids: ids) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + var currentItems = transaction.getStoryItems(peerId: peerId) + for i in 0 ..< currentItems.count { + if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) { + if case .item = updatedItem { + if let entry = CodableEntry(updatedItem) { + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id) + } + } + } + } + transaction.setStoryItems(peerId: peerId, items: currentItems) + + for id in ids { + let current = transaction.getStory(id: StoryId(peerId: peerId, id: id)) + var updated: CodableEntry? + if let updatedItem = result.first(where: { $0.id == id }) { + if let entry = CodableEntry(updatedItem) { + updated = entry + } + } + if current != updated { + transaction.setStory(id: StoryId(peerId: peerId, id: id), value: updated ?? CodableEntry(data: Data())) + } + } + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 805b7d9fc0..a3b9667b44 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -862,23 +862,7 @@ public extension TelegramEngine { } public func refreshStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { - return _internal_getStoriesById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, ids: ids) - |> mapToSignal { result -> Signal in - return self.account.postbox.transaction { transaction -> Void in - var currentItems = transaction.getStoryItems(peerId: peerId) - for i in 0 ..< currentItems.count { - if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) { - if case .item = updatedItem { - if let entry = CodableEntry(updatedItem) { - currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id) - } - } - } - } - transaction.setStoryItems(peerId: peerId, items: currentItems) - } - |> ignoreValues - } + return _internal_refreshStories(account: self.account, peerId: peerId, ids: ids) } public func refreshStoryViews(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index b7b71bc3bf..2c21713e0a 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -42,9 +42,21 @@ public final class AvatarStoryIndicatorComponent: Component { self.component = component self.state = state - let lineWidth: CGFloat = 3.0 + let lineWidth: CGFloat + let diameter: CGFloat + let outerInset: CGFloat - self.indicatorView.image = generateImage(CGSize(width: availableSize.width + lineWidth * 2.0, height: availableSize.width + lineWidth * 2.0), rotatedContext: { size, context in + if component.hasUnseen { + lineWidth = 3.0 + outerInset = 3.0 + lineWidth + diameter = availableSize.width + outerInset * 2.0 + } else { + lineWidth = 2.0 + outerInset = 3.0 + lineWidth + diameter = availableSize.width + outerInset * 2.0 + } + + self.indicatorView.image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setLineWidth(lineWidth) @@ -65,7 +77,7 @@ public final class AvatarStoryIndicatorComponent: Component { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) - transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(), size: availableSize).insetBy(dx: -lineWidth * 2.0, dy: -lineWidth * 2.0)) + transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(), size: availableSize).insetBy(dx: -outerInset, dy: -outerInset)) return availableSize } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Sources/ChatHistoryEntry.swift index abe410c220..113aa42e3d 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntry.swift @@ -137,6 +137,20 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } } + if lhsMessage.associatedStories.count != rhsMessage.associatedStories.count { + return false + } + if !lhsMessage.associatedStories.isEmpty { + for (id, story) in lhsMessage.associatedStories { + if let otherStory = rhsMessage.associatedStories[id] { + if story != otherStory { + return false + } + } else { + return false + } + } + } if lhsSelection != rhsSelection { return false } @@ -194,6 +208,20 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } } + if lhsMessage.associatedStories.count != rhsMessage.associatedStories.count { + return false + } + if !lhsMessage.associatedStories.isEmpty { + for (id, story) in lhsMessage.associatedStories { + if let otherStory = rhsMessage.associatedStories[id] { + if story != otherStory { + return false + } + } else { + return false + } + } + } if lhsAttributes != rhsAttributes { return false } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 46d8f9a1c6..c932bd4a69 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -523,6 +523,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let unseenReactionsProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2, submitInterval: 0.0) private let extendedMediaProcessingManager = ChatMessageVisibleThrottledProcessingManager(interval: 5.0) private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) + private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager() let prefetchManager: InChatPrefetchManager private var currentEarlierPrefetchMessages: [(Message, Media)] = [] @@ -793,6 +794,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.refreshMediaProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: messageIds) } + self.refreshStoriesProcessingManager.process = { [weak context] messageIds in + context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: messageIds) + } self.translationProcessingManager.process = { [weak self, weak context] messageIds in if let context = context, let toLang = self?.toLang { let _ = translateMessageIds(context: context, messageIds: Array(messageIds), toLang: toLang).start() @@ -2043,6 +2047,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageIdsWithLiveLocation: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithRefreshMedia: [MessageId] = [] + var messageIdsWithRefreshStories: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messageIdsWithUnseenReactions: [MessageId] = [] var messageIdsWithInactiveExtendedMedia = Set() @@ -2068,6 +2073,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var contentRequiredValidation = false var mediaRequiredValidation = false var hasUnseenReactions = false + var storiesRequiredValidation = false for attribute in message.attributes { if attribute is ViewCountMessageAttribute { if message.id.namespace == Namespaces.Message.Cloud { @@ -2126,6 +2132,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if invoice.version != TelegramMediaInvoice.lastVersion { contentRequiredValidation = true } + } else if let story = media as? TelegramMediaStory { + if message.associatedStories[story.storyId] == nil { + storiesRequiredValidation = true + } } } if contentRequiredValidation { @@ -2134,6 +2144,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if mediaRequiredValidation { messageIdsWithRefreshMedia.append(message.id) } + if storiesRequiredValidation { + messageIdsWithRefreshStories.append(message.id) + } if hasUnconsumedMention && !hasUnconsumedContent { messageIdsWithUnseenPersonalMention.append(message.id) } @@ -2329,6 +2342,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !messageIdsWithRefreshMedia.isEmpty { self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia) } + if !messageIdsWithRefreshStories.isEmpty { + self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories) + } if !messageIdsWithUnseenPersonalMention.isEmpty { self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index c5f75a54ed..b98c6e659c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -586,6 +586,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } + } else if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let media = item.media { + if let media = media as? TelegramMediaFile { + messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media) + } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { + messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) + } + } } } if let cancel = self.fetchControls.with({ return $0?.cancel }) { @@ -615,6 +623,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia } + + if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let mediaValue = item.media { + media = mediaValue + } + } + videoContentMatch = self.message?.stableId == stableId && media?.id == mediaId } self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default) @@ -626,6 +641,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { if let invoice = self.media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { self.activateLocalContent(.default) + } else if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let mediaValue = item.media { + let _ = mediaValue + self.activateLocalContent(.default) + } } else { self.progressPressed(canActivate: true) } @@ -682,7 +702,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var maxHeight = layoutConstants.image.maxDimensions.height var unboundSize: CGSize - if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { + if let _ = media as? TelegramMediaStory { + unboundSize = CGSize(width: 1080, height: 1920) + } else if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5))) } else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions { if let thumbnail = file.previewRepresentations.first { @@ -888,6 +910,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var mediaUpdated = false if let currentMedia = currentMedia { mediaUpdated = !media.isSemanticallyEqual(to: currentMedia) + + if !mediaUpdated, let media = media as? TelegramMediaStory { + if message.associatedStories[media.storyId] != currentMessage?.associatedStories[media.storyId] { + mediaUpdated = true + } + } } else { mediaUpdated = true } @@ -958,7 +986,134 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - if let image = media as? TelegramMediaImage { + if let story = media as? TelegramMediaStory { + if hasCurrentVideoNode { + replaceVideoNode = true + } + if hasCurrentAnimatedStickerNode { + replaceAnimatedStickerNode = true + } + + if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = item.media { + if let image = media as? TelegramMediaImage { + if hasCurrentVideoNode { + replaceVideoNode = true + } + if hasCurrentAnimatedStickerNode { + replaceAnimatedStickerNode = true + } + if isSecretMedia { + updateImageSignal = { synchronousLoad, _ in + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) + } + } else { + updateImageSignal = { synchronousLoad, highQuality in + return chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: true) + } + } + + updatedFetchControls = FetchControls(fetch: { manual in + if let strongSelf = self { + if let representation = largestRepresentationForPhoto(image) { + strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, range: representationFetchRangeForDisplayAtSize(representation: representation, dimension: nil/*isSecretMedia ? nil : 600*/), userInitiated: manual, storeToDownloadsPeerId: storeToDownloadsPeerId).start()) + } + } + }, cancel: { + chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) + if let resource = largestRepresentationForPhoto(image)?.resource { + messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: resource) + } + }) + } else if let file = media as? TelegramMediaFile { + if isSecretMedia { + updateImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) + } + } else { + if file.isAnimatedSticker { + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = { synchronousLoad, _ in + return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) + } + } else if file.isSticker || file.isVideoSticker { + updateImageSignal = { synchronousLoad, _ in + return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) + } + } else { + onlyFullSizeVideoThumbnail = isSendingUpdated + updateImageSignal = { synchronousLoad, _ in + return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) + } + } + } + + var uploading = false + if file.resource is VideoLibraryMediaResource { + uploading = true + } + + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { + updateVideoFile = file + if hasCurrentVideoNode { + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true + } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { + replaceVideoNode = true + } + } + } else if !(file.resource is LocalFileVideoMediaResource) { + replaceVideoNode = true + } + } else { + if hasCurrentVideoNode { + replaceVideoNode = false + } + + if file.isAnimatedSticker || file.isVideoSticker { + updateAnimatedStickerFile = file + if hasCurrentAnimatedStickerNode { + if let currentMedia = currentMedia { + if !currentMedia.isSemanticallyEqual(to: file) { + replaceAnimatedStickerNode = true + } + } else { + replaceAnimatedStickerNode = true + } + } else { + replaceAnimatedStickerNode = true + } + } + } + + updatedFetchControls = FetchControls(fetch: { manual in + if let strongSelf = self { + if file.isAnimated { + strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) + } else { + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, storeToDownloadsPeerId: storeToDownloadsPeerId).start()) + } + } + }, cancel: { + if file.isAnimated { + context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + } else { + messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) + } + }) + } + } + } else if let image = media as? TelegramMediaImage { if hasCurrentVideoNode { replaceVideoNode = true } @@ -1149,6 +1304,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio isExtendedMedia = true media = fullMedia } + if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let mediaValue = item.media { + media = mediaValue + } + } if let image = media as? TelegramMediaImage { if message.flags.isSending { @@ -1426,6 +1586,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia } + if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let mediaValue = item.media { + media = mediaValue + } + } if case .full = automaticDownload { if let _ = media as? TelegramMediaImage { @@ -1670,6 +1835,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia } + if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let mediaValue = item.media { + media = mediaValue + } + } switch fetchStatus { case let .Fetching(_, progress): diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index ac979bf2a4..4f99945eb8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -92,14 +92,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { automaticDownload = .full } } else if let telegramStory = media as? TelegramMediaStory { + selectedMedia = telegramStory if let storyMedia = item.message.associatedStories[telegramStory.storyId], case let .item(storyItem) = storyMedia.get(Stories.StoredItem.self), let media = storyItem.media { if let telegramImage = media as? TelegramMediaImage { - selectedMedia = telegramImage if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { automaticDownload = .full } } else if let telegramFile = media as? TelegramMediaFile { - selectedMedia = telegramFile if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { automaticDownload = .full } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 948ed554ab..888c1da692 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -23,8 +23,35 @@ import UndoUI import WebsiteType import GalleryData import StoryContainerScreen +import StoryContentComponent func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { + 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 + } + } + 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 65b57d9a9d..80d8d560c5 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -3415,6 +3415,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateSublayerTransformScale(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) transition.updateSublayerTransformScale(node: self.avatarOverlayNode, scale: avatarScale) } + + if let avatarStoryView = self.avatarListNode.avatarContainerNode.avatarStoryView?.view { + transition.updateAlpha(layer: avatarStoryView.layer, alpha: 1.0 - transitionFraction) + } + let apparentAvatarFrame: CGRect let controlsClippingFrame: CGRect if self.isAvatarExpanded {