diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 01f9576c49..3820e7fd4d 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -190,6 +190,10 @@ public final class SparseItemGrid: ASDisplayNode { open var holeAnchor: HoleAnchor { preconditionFailure() } + + open var isReorderable: Bool { + return false + } public init() { } @@ -1148,7 +1152,7 @@ public final class SparseItemGrid: ASDisplayNode { var itemScale: CGFloat let itemCornerRadius: CGFloat - if self.isReordering { + if self.isReordering && item.isReorderable { itemScale = (itemFrame.height - 6.0 * 2.0) / itemFrame.height itemCornerRadius = 10.0 } else { @@ -1240,7 +1244,7 @@ public final class SparseItemGrid: ASDisplayNode { if let layer = item.layer { layer.update(size: layer.bounds.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) - if self.isReordering { + if self.isReordering, let contentItem, contentItem.isReorderable { if layer.animation(forKey: "shaking_position") == nil { startShaking(layer: layer) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 99b21d5b95..53a42aa58e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -565,7 +565,7 @@ public struct StoryListContextState: Equatable { public var peerReference: PeerReference? public var items: [Item] - public var pinnedIds: Set + public var pinnedIds: [Int32] public var totalCount: Int public var loadMoreToken: AnyHashable? public var isCached: Bool @@ -575,7 +575,7 @@ public struct StoryListContextState: Equatable { public init( peerReference: PeerReference?, items: [Item], - pinnedIds: Set, + pinnedIds: [Int32], totalCount: Int, loadMoreToken: AnyHashable?, isCached: Bool, @@ -633,7 +633,7 @@ public final class PeerStoryListContext: StoryListContext { self.peerId = peerId self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in let key = ValueBoxKey(length: 8 + 1) @@ -723,17 +723,19 @@ public final class PeerStoryListContext: StoryListContext { return } - var updatedState = State(peerReference: peerReference, items: items, pinnedIds: Set(pinnedIds), totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) + var updatedState = State(peerReference: peerReference, items: items, pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) self.stateValue = updatedState @@ -862,7 +864,7 @@ public final class PeerStoryListContext: StoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: count)) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } } @@ -1124,14 +1126,15 @@ public final class PeerStoryListContext: StoryListContext { author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1180,14 +1183,15 @@ public final class PeerStoryListContext: StoryListContext { author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1204,18 +1208,19 @@ public final class PeerStoryListContext: StoryListContext { case let .updatePinnedToTopList(peerId, ids): if self.peerId == peerId && !self.isArchived { let previousIds = (finalUpdatedState ?? self.stateValue).pinnedIds - if previousIds != Set(ids) { + if previousIds != ids { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.pinnedIds = Set(ids) + updatedState.pinnedIds = ids updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1235,7 +1240,7 @@ public final class PeerStoryListContext: StoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: Int32(totalCount))) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount))) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } }).start() @@ -1308,7 +1313,7 @@ public final class SearchStoryListContext: StoryListContext { self.account = account self.source = source - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) self.statePromise.set(.single(self.stateValue)) self.loadMore(completion: nil) @@ -2107,7 +2112,7 @@ public final class BotPreviewStoryListContext: StoryListContext { self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) let localStateKey: PostboxViewKey = .storiesState(key: .local) @@ -2222,7 +2227,7 @@ public final class BotPreviewStoryListContext: StoryListContext { self.stateValue = State( peerReference: (peer?._asPeer()).flatMap(PeerReference.init), items: items, - pinnedIds: Set(), + pinnedIds: [], totalCount: items.count, loadMoreToken: nil, isCached: botPreview != nil, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index ca0ea8fa43..6dd583ca77 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1305,7 +1305,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } - if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), botInfo.flags.contains(.canEdit) { availablePanes?.insert(.botPreview, at: 0) } else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.media.isEmpty { availablePanes?.insert(.botPreview, at: 0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 64f4f15f36..ba14a31620 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -92,12 +92,16 @@ private final class VisualMediaItem: SparseItemGrid.Item { override var index: Int { return self.indexValue } + override var isReorderable: Bool { + return self.isReorderableValue + } let localMonthTimestamp: Int32 let peer: PeerReference let storyId: StoryId let story: EngineStoryItem let authorPeer: EnginePeer? let isPinned: Bool + let isReorderableValue: Bool override var id: AnyHashable { return AnyHashable(self.storyId) @@ -111,7 +115,7 @@ private final class VisualMediaItem: SparseItemGrid.Item { return VisualMediaHoleAnchor(index: self.index, storyId: self.storyId, localMonthTimestamp: self.localMonthTimestamp) } - init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32) { + init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32, isReorderable: Bool) { self.indexValue = index self.peer = peer self.storyId = storyId @@ -119,6 +123,7 @@ private final class VisualMediaItem: SparseItemGrid.Item { self.authorPeer = authorPeer self.isPinned = isPinned self.localMonthTimestamp = localMonthTimestamp + self.isReorderableValue = isReorderable } } @@ -2453,6 +2458,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) + if isPinned { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.beginReordering() + }) + }))) + } } items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2769,6 +2786,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr continue } + var isReorderable = false + switch self.scope { + case .botPreview: + isReorderable = !item.storyItem.isPending + case let .peer(id, _, _): + if id == self.context.account.peerId { + isReorderable = state.pinnedIds.contains(item.storyItem.id) + } + default: + break + } + mappedItems.append(VisualMediaItem( index: mappedItems.count, peer: peerReference, @@ -2776,7 +2805,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr story: item.storyItem, authorPeer: item.peer, isPinned: state.pinnedIds.contains(item.storyItem.id), - localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue + localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, + isReorderable: isReorderable )) } if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { @@ -2809,7 +2839,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let currentSynchronous = synchronous && firstTime let currentReloadAtTop = reloadAtTop && firstTime - self.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) + self.updateHistory(items: items, pinnedIds: Set(state.pinnedIds), synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) } private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool) { @@ -2845,13 +2875,31 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { - if case .botPreview = self.scope, let items = self.items, let item = item as? VisualMediaItem { + if let items = self.items, let item = item as? VisualMediaItem { + var toIndex = toIndex + if case .botPreview = self.scope { + } else if case let .peer(id, _, _) = self.scope { + if id == self.context.account.peerId { + let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) + if let maxPinnedIndex { + toIndex = min(toIndex, maxPinnedIndex) + } else { + return + } + } + } else { + return + } + guard let toItem = items.items.first(where: { $0.index == toIndex }) as? VisualMediaItem else { return } if item.story.isPending || toItem.story.isPending { return } + if !item.isReorderable { + return + } var ids = items.items.compactMap { item -> StoryId? in return (item as? VisualMediaItem)?.storyId @@ -3980,6 +4028,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.reorderedIds = nil if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { listSource.reorderItems(ids: reorderedIds) + } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { + var updatedPinnedIds: [Int32] = [] + for id in reorderedIds { + inner: for item in items.items { + if let item = item as? VisualMediaItem { + if item.storyId == id { + if item.isPinned { + updatedPinnedIds.append(id.id) + break inner + } + } + } + } + } + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() } } }