diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3373613912..f270967963 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -95,7 +95,22 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem { + func item( + context: AccountContext, + presentationData: ChatListPresentationData, + filter: ChatListNodePeersFilter, + key: ChatListSearchPaneKey, + peerSelected: @escaping (EnginePeer, Int64?) -> Void, + disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, + peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, + clearRecentlySearchedPeers: @escaping () -> Void, + deletePeer: @escaping (EnginePeer.Id) -> Void, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, + isChannelsTabExpanded: Bool, + toggleChannelsTabExpanded: @escaping () -> Void + ) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in @@ -224,9 +239,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { if case .channels = key { if case .recommendedChannels = section { //TODO:localize - header = ChatListSearchItemHeader(type: .text("RECOMMENDED CHANNELS", 0), theme: theme, strings: strings) + header = ChatListSearchItemHeader(type: .text("RECOMMENDED CHANNELS", 1), theme: theme, strings: strings) } else { - header = nil + //TODO:localize + header = ChatListSearchItemHeader(type: .text("CHANNELS YOU JOINED", 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? "Show less" : "Show more", action: { + toggleChannelsTabExpanded() + }) } } else { header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { @@ -944,12 +962,30 @@ public struct ChatListSearchContainerTransition { } } -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerRecentTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) +private func chatListSearchContainerPreparedRecentTransition( + from fromEntries: [ChatListRecentEntry], + to toEntries: [ChatListRecentEntry], + forceUpdateAll: Bool, + context: AccountContext, + presentationData: ChatListPresentationData, + filter: ChatListNodePeersFilter, + key: ChatListSearchPaneKey, + peerSelected: @escaping (EnginePeer, Int64?) -> Void, + disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, + peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, + clearRecentlySearchedPeers: @escaping () -> Void, + deletePeer: @escaping (EnginePeer.Id) -> Void, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, + isChannelsTabExpanded: Bool, + toggleChannelsTabExpanded: @escaping () -> Void +) -> ChatListSearchContainerRecentTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdateAll) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -2744,7 +2780,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } })) - let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) + let previousRecentItemsValue = Atomic(value: nil) let hasRecentPeers: Signal if case .channels = key { hasRecentPeers = .single(false) @@ -2761,7 +2797,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> distinctUntilChanged } - var recentItems = combineLatest( + struct RecentItems { + var entries: [ChatListRecentEntry] + var isChannelsTabExpanded: Bool + var recommendedChannelOrder: [EnginePeer.Id] + } + + let isChannelsTabExpandedValue = ValuePromise(false, ignoreRepeated: true) + let toggleChannelsTabExpanded: () -> Void = { + let _ = (isChannelsTabExpandedValue.get() |> take(1)).startStandalone(next: { value in + isChannelsTabExpandedValue.set(!value) + }) + } + + var recentItems: Signal = combineLatest( hasRecentPeers, fixedRecentlySearchedPeers |> mapToSignal { peers -> Signal<([RecentlySearchedPeer], [EnginePeer.Id: PeerStoryStats], [EnginePeer.Id: Bool], Set), NoError> in return context.engine.data.subscribe( @@ -2804,7 +2853,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { presentationDataPromise.get(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()) ) - |> mapToSignal { hasRecentPeers, peersAndStories, presentationData, globalNotificationSettings -> Signal<[ChatListRecentEntry], NoError> in + |> mapToSignal { hasRecentPeers, peersAndStories, presentationData, globalNotificationSettings -> Signal in let (peers, peerStoryStats, requiresPremiumForMessaging, refreshIsPremiumRequiredForMessaging) = peersAndStories if !refreshIsPremiumRequiredForMessaging.isEmpty { @@ -2834,17 +2883,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - return .single(entries) + return .single(RecentItems(entries: entries, isChannelsTabExpanded: false, recommendedChannelOrder: [])) } if peersFilter.contains(.excludeRecent) { - recentItems = .single([]) + recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: false, recommendedChannelOrder: [])) } if case .savedMessagesChats = location { - recentItems = .single([]) + recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: false, recommendedChannelOrder: [])) } if case .channels = key { - let localChannels = context.engine.messages.getAllLocalChannels() + struct LocalChannels { + var peerIds: [EnginePeer.Id] + var isExpanded: Bool + } + let localChannels = isChannelsTabExpandedValue.get() + |> mapToSignal { isChannelsTabExpanded -> Signal in + return context.engine.messages.getAllLocalChannels(count: isChannelsTabExpanded ? 500 : 5) + |> map { peerIds -> LocalChannels in + return LocalChannels(peerIds: peerIds, isExpanded: isChannelsTabExpanded) + } + } + let remoteChannels: Signal = context.engine.peers.recommendedChannels(peerId: nil) let _ = self.context.engine.peers.requestGlobalRecommendedChannelsIfNeeded().startStandalone() @@ -2853,16 +2913,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { localChannels, remoteChannels ) - |> mapToSignal { localChannelIds, remoteChannels -> Signal<[ChatListRecentEntry], NoError> in - var allChannelIds = localChannelIds + |> mapToSignal { localChannels, remoteChannels -> Signal in + var allChannelIds = localChannels.peerIds + let isChannelsTabExpanded = localChannels.isExpanded var cachedSubscribers: [EnginePeer.Id: Int32] = [:] + var recommendedChannelOrder: [EnginePeer.Id] = [] if let remoteChannels { for channel in remoteChannels.channels { if !allChannelIds.contains(channel.peer.id) { allChannelIds.append(channel.peer.id) } cachedSubscribers[channel.peer.id] = channel.subscribers + recommendedChannelOrder.append(channel.peer.id) } } @@ -2882,6 +2945,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) } ), + EngineDataMap( + allChannelIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.StoryStats in + return TelegramEngine.EngineData.Item.Peer.StoryStats(id: peerId) + } + ), EngineDataMap( allChannelIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.ParticipantCount in return TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId) @@ -2889,11 +2957,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { ), TelegramEngine.EngineData.Item.NotificationSettings.Global() ) - |> map { peers, notificationSettings, unreadCounts, participantCounts, globalNotificationSettings -> [ChatListRecentEntry] in + |> map { peers, notificationSettings, unreadCounts, storyStats, participantCounts, globalNotificationSettings -> RecentItems in var result: [ChatListRecentEntry] = [] var existingIds = Set() - for id in localChannelIds { + for id in localChannels.peerIds { if existingIds.contains(id) { continue } @@ -2908,6 +2976,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else if let count = cachedSubscribers[id] { subpeerSummary = RecentlySearchedPeerSubpeerSummary(count: Int(count)) } + var peerStoryStats: PeerStoryStats? + if let value = storyStats[peer.id] { + peerStoryStats = value + } result.append(.peer( index: result.count, peer: RecentlySearchedPeer( @@ -2924,7 +2996,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalNotificationSettings, - nil, + peerStoryStats, false )) } @@ -2944,6 +3016,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else if let count = cachedSubscribers[channel.peer.id] { subpeerSummary = RecentlySearchedPeerSubpeerSummary(count: Int(count)) } + var peerStoryStats: PeerStoryStats? + if let value = storyStats[peer.id] { + peerStoryStats = value + } result.append(.peer( index: result.count, peer: RecentlySearchedPeer( @@ -2960,13 +3036,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalNotificationSettings, - nil, + peerStoryStats, false )) } } - return result + return RecentItems(entries: result, isChannelsTabExpanded: isChannelsTabExpanded, recommendedChannelOrder: recommendedChannelOrder) } } } @@ -2979,18 +3055,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { presentationDataPromise.get(), recentItems ) - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, entries in + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recentItems in if let strongSelf = self { - let previousEntries = previousRecentItems.swap(entries) + let previousRecentItems = previousRecentItemsValue.swap(recentItems) - var firstTime = previousEntries == nil - if let previousEntries { - if previousEntries.count < entries.count { + var firstTime = previousRecentItems == nil + var forceUpdateAll = false + if let previousRecentItems { + if previousRecentItems.entries.count < recentItems.entries.count { firstTime = true } + if previousRecentItems.recommendedChannelOrder != recentItems.recommendedChannelOrder { + firstTime = true + } + if previousRecentItems.isChannelsTabExpanded != recentItems.isChannelsTabExpanded { + firstTime = true + forceUpdateAll = true + } } - let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId in + let transition = chatListSearchContainerPreparedRecentTransition(from: previousRecentItems?.entries ?? [], to: recentItems.entries, forceUpdateAll: forceUpdateAll, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId in guard let self else { return } @@ -2998,7 +3082,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case .channels = key { if let navigationController = self.navigationController { var customChatNavigationStack: [EnginePeer.Id] = [] - if let entries = previousRecentItems.with({ $0 }) { + if let entries = previousRecentItemsValue.with({ $0 })?.entries { for entry in entries { if case let .peer(_, peer, _, _, _, _, _, _, _, _, _) = entry { customChatNavigationStack.append(peer.peer.peerId) @@ -3040,6 +3124,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).startStandalone() }, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer, openStories: { peerId, avatarNode in interaction.openStories?(peerId, avatarNode) + }, + isChannelsTabExpanded: recentItems.isChannelsTabExpanded, + toggleChannelsTabExpanded: { + toggleChannelsTabExpanded() }) strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 569096a4fd..591793299e 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -1438,7 +1438,9 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) - entries.append(.browserExperiment(experimentalSettings.browserExperiment)) + if sharedContext.applicationBindings.appBuildType == .internal { + entries.append(.browserExperiment(experimentalSettings.browserExperiment)) + } entries.append(.localTranscription(experimentalSettings.localTranscription)) if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 6bf6c757aa..3cd072ebc3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1411,7 +1411,7 @@ public extension TelegramEngine { return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option) } - public func getAllLocalChannels() -> Signal<[EnginePeer.Id], NoError> { + public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in var result: [EnginePeer.Id] = [] @@ -1444,7 +1444,7 @@ public extension TelegramEngine { } filteredResult.append(id) - if filteredResult.count >= 5 { + if filteredResult.count >= count { break } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 93812e8b40..08f2ddfbe7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -419,7 +419,18 @@ private final class PeerInfoPendingPane { let paneNode: PeerInfoPaneNode switch key { case .stories, .storyArchive: - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) + var canManage = false + if let peer = data.peer { + if peer.id == context.account.peerId { + canManage = true + } else if let channel = peer as? TelegramChannel { + if channel.hasPermission(.editStories) { + canManage = true + } + } + } + + let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 1e877ef2cc..4f810d6f33 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -462,6 +462,7 @@ final class PeerInfoStoryGridScreenComponent: Component { isSaved: true, isArchive: component.scope == .archive, isProfileEmbedded: false, + canManageStories: true, navigationController: { [weak self] in guard let self else { return nil diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 8db423797a..f8232cb5de 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1185,6 +1185,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let isSaved: Bool private let isArchive: Bool private let isProfileEmbedded: Bool + private let canManageStories: Bool public private(set) var contentType: ContentType private var contentTypePromise: ValuePromise @@ -1298,7 +1299,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private weak var contextControllerToDismissOnSelection: ContextControllerProtocol? private weak var tempContextContentItemNode: TempExtractedItemNode? - public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { + public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context self.peerId = peerId self.chatLocation = chatLocation @@ -1308,6 +1309,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.isSaved = isSaved self.isArchive = isArchive self.isProfileEmbedded = isProfileEmbedded + self.canManageStories = canManageStories self.isSelectionModeActive = !isProfileEmbedded && isArchive @@ -1657,12 +1659,25 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr guard let item = strongSelf.itemGrid.item(at: point) else { return false } + guard let layer = item.layer as? ItemLayer else { + return false + } + guard let storyItem = layer.item else { + return false + } if let result = strongSelf.view.hitTest(point, with: nil) { if result.asyncdisplaykit_node is SparseItemGridScrollingArea { return false } } + + if !strongSelf.canManageStories { + if !storyItem.story.isForwardingDisabled, case .everyone = storyItem.story.privacy?.base { + } else { + return false + } + } strongSelf.currentGestureItem = item @@ -1768,167 +1783,190 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func openContextMenu(item: EngineStoryItem, itemLayer: ItemLayer, rect: CGRect, gesture: ContextGesture?) { - guard let parentController = self.parentController else { - return - } - - var items: [ContextMenuItem] = [] - - //TODO:localize - - items.append(.action(ContextMenuActionItem(text: !self.isArchive ? "Archive" : "Unarchive", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { - f(.default) + return + } + guard let parentController = self.parentController else { return } - if self.isArchive { - f(.default) - } else { - f(.dismissWithoutContent) - } + let canManage = self.canManageStories - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: self.peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() - }))) - - if !self.isArchive { - let isPinned = self.pinnedIds.contains(item.id) - items.append(.action(ContextMenuActionItem(text: isPinned ? "Unpin" : "Pin", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in - itemLayer?.isHidden = false - guard let self else { - f(.default) - return - } - - if !isPinned && self.pinnedIds.count >= 3 { - f(.default) - - let presentationData = self.presentationData - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can't pin more than 3 posts.", timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - - return - } - - f(.dismissWithoutContent) - - var updatedPinnedIds = self.pinnedIds - if isPinned { - updatedPinnedIds.remove(item.id) - } else { - updatedPinnedIds.insert(item.id) - } - let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: self.peerId, ids: Array(updatedPinnedIds)).startStandalone() - - //TODO:localize - let presentationData = self.presentationData - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: isPinned ? nil : "Story Pinned", text: isPinned ? "Story Unpinned." : "Now it will always be shown on the top.", cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }))) - } - - /*items.append(.action(ContextMenuActionItem(text: "Edit", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return - } - let _ = self - - - }) - })))*/ - - if !item.isForwardingDisabled { - items.append(.action(ContextMenuActionItem(text: "Forward", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { + var items: [ContextMenuItem] = [] + + //TODO:localize + + if canManage { + items.append(.action(ContextMenuActionItem(text: !self.isArchive ? "Archive" : "Unarchive", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in guard let self else { + f(.default) return } - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + if self.isArchive { + f(.default) + } else { + f(.dismissWithoutContent) + } + + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: self.peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: self.isArchive ? "Story unarchived." : "Story archived.", cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) + + if !self.isArchive { + let isPinned = self.pinnedIds.contains(item.id) + items.append(.action(ContextMenuActionItem(text: isPinned ? "Unpin" : "Pin", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in + itemLayer?.isHidden = false guard let self else { - return - } - guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + f(.default) return } - let shareController = ShareController( - context: self.context, - subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: self.peerId, id: item.id), isMention: false))), - presetText: nil, - preferredAction: .default, - showInChat: nil, - fromForeignApp: false, - segmentedValues: nil, - externalShare: false, - immediateExternalShare: false, - switchableAccounts: [], - immediatePeerId: nil, - updatedPresentationData: nil, - forceTheme: nil, - forcedActionTitle: nil, - shareAsLink: false, - collectibleItemInfo: nil - ) - self.parentController?.present(shareController, in: .window(.root)) - }) - }) - }))) - } - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return + if !isPinned && self.pinnedIds.count >= 3 { + f(.default) + + let presentationData = self.presentationData + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can't pin more than 3 posts.", timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + + return + } + + f(.dismissWithoutContent) + + var updatedPinnedIds = self.pinnedIds + if isPinned { + updatedPinnedIds.remove(item.id) + } else { + updatedPinnedIds.insert(item.id) + } + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: self.peerId, ids: Array(updatedPinnedIds)).startStandalone() + + //TODO:localize + let presentationData = self.presentationData + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: isPinned ? nil : "Story Pinned", text: isPinned ? "Story Unpinned." : "Now it will always be shown on the top.", cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) } - self.presentDeleteConfirmation(ids: Set([item.id])) - }) - }))) - - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, f in - guard let self, let parentController = self.parentController as? PeerInfoScreen else { - f(.default) + /*items.append(.action(ContextMenuActionItem(text: "Edit", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self else { + return + } + let _ = self + + + }) + })))*/ + } + + if !item.isForwardingDisabled, case .everyone = item.privacy?.base { + items.append(.action(ContextMenuActionItem(text: "Forward", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self else { + return + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self else { + return + } + guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + return + } + + let shareController = ShareController( + context: self.context, + subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: self.peerId, id: item.id), isMention: false))), + presetText: nil, + preferredAction: .default, + showInChat: nil, + fromForeignApp: false, + segmentedValues: nil, + externalShare: false, + immediateExternalShare: false, + switchableAccounts: [], + immediatePeerId: nil, + updatedPresentationData: nil, + forceTheme: nil, + forcedActionTitle: nil, + shareAsLink: false, + collectibleItemInfo: nil + ) + self.parentController?.present(shareController, in: .window(.root)) + }) + }) + }))) + } + + if canManage { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self else { + return + } + + self.presentDeleteConfirmation(ids: Set([item.id])) + }) + }))) + } + + if self.canManageStories { + if !items.isEmpty { + items.append(.separator) + } + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, f in + guard let self, let parentController = self.parentController as? PeerInfoScreen else { + f(.default) + return + } + + self.contextControllerToDismissOnSelection = c + parentController.toggleStorySelection(ids: [item.id], isSelected: true) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in + guard let self, let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection else { + return + } + if let contextControllerToDismissOnSelection = contextControllerToDismissOnSelection as? ContextController { + contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) + } + }) + }))) + } + + if items.isEmpty { return } - self.contextControllerToDismissOnSelection = c - parentController.toggleStorySelection(ids: [item.id], isSelected: true) + let tempSourceNode = TempExtractedItemNode( + item: item, + itemLayer: itemLayer + ) + tempSourceNode.frame = rect + tempSourceNode.update(size: rect.size) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in - guard let self, let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection else { - return - } - if let contextControllerToDismissOnSelection = contextControllerToDismissOnSelection as? ContextController { - contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) - } - }) - }))) - - let tempSourceNode = TempExtractedItemNode( - item: item, - itemLayer: itemLayer - ) - tempSourceNode.frame = rect - tempSourceNode.update(size: rect.size) - - let scaleSide = itemLayer.bounds.width - let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) - let currentScale = minScale - - ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: tempSourceNode.contextSourceNode.contentNode, scale: currentScale) - ContainedViewLayoutTransition.immediate.updateTransformScale(layer: itemLayer, scale: 1.0) - - self.tempContextContentItemNode = tempSourceNode - self.addSubnode(tempSourceNode) - - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - parentController.presentInGlobalOverlay(contextController) + let scaleSide = itemLayer.bounds.width + let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) + let currentScale = minScale + + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: tempSourceNode.contextSourceNode.contentNode, scale: currentScale) + ContainedViewLayoutTransition.immediate.updateTransformScale(layer: itemLayer, scale: 1.0) + + self.tempContextContentItemNode = tempSourceNode + self.addSubnode(tempSourceNode) + + let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + parentController.presentInGlobalOverlay(contextController) + }) } public func updateContentType(contentType: ContentType) { @@ -2400,7 +2438,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var bottomInset = bottomInset - if let selectedIds = self.itemInteraction.selectedIds { + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories { let selectionPanel: ComponentView var selectionPanelTransition = Transition(transition) if let current = self.selectionPanel { @@ -2484,6 +2522,22 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } let _ = self.context.engine.messages.updateStoriesArePinned(peerId: self.peerId, ids: items, isPinned: self.isArchive ? true : false).startStandalone() + + let text: String + if self.isArchive { + if items.count == 1 { + text = "Story unarchived." + } else { + text = "Stories unarchived." + } + } else { + if items.count == 1 { + text = "Story archived." + } else { + text = "Stories archived." + } + } + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } )) selectionItems.append(BottomActionsPanelComponent.Item(