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) }))