Open stories from search

This commit is contained in:
Ali 2023-08-03 15:22:35 +03:00
parent 052311553e
commit 9a50913b57
7 changed files with 224 additions and 83 deletions

View File

@ -2347,7 +2347,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
self?.controller?.present(c, in: .window(.root), with: a)
}, presentInGlobalOverlay: { [weak self] c, a in
self?.controller?.presentInGlobalOverlay(c, with: a)
}, navigationController: navigationController)
}, navigationController: navigationController, parentController: { [weak self] in
return self?.controller
})
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {

View File

@ -34,6 +34,8 @@ import TelegramAnimatedStickerNode
import AnimationCache
import MultiAnimationRenderer
import PremiumUI
import AvatarNode
import StoryContainerScreen
private enum ChatListTokenId: Int32 {
case archive
@ -57,8 +59,9 @@ final class ChatListSearchInteraction {
let present: (ViewController, Any?) -> Void
let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
let openStories: ((PeerId, ASDisplayNode) -> Void)?
init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?, openStories: ((PeerId, ASDisplayNode) -> Void)?) {
self.openPeer = openPeer
self.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage
@ -72,6 +75,7 @@ final class ChatListSearchInteraction {
self.present = present
self.dismissInput = dismissInput
self.getSelectedMessageIds = getSelectedMessageIds
self.openStories = openStories
}
}
@ -140,7 +144,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var validLayout: (ContainerViewLayout, CGFloat)?
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) {
private let sharedOpenStoryDisposable = MetaDisposable()
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?, parentController: @escaping () -> ViewController?) {
var initialFilter = initialFilter
if case .chats = initialFilter, case .forum = location {
initialFilter = .topics
@ -258,6 +264,20 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} else {
return nil
}
}, openStories: { [weak self] peerId, sourceNode in
guard let self else {
return
}
guard let parentController = parentController() else {
return
}
StoryContainerScreen.openPeerStories(
context: context,
peerId: peerId,
parentController: parentController,
avatarNode: sourceNode as? AvatarNode,
sharedProgressDisposable: self.sharedOpenStoryDisposable
)
})
self.paneContainerNode.interaction = interaction
@ -500,6 +520,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.presentationDataDisposable?.dispose()
self.suggestedFiltersDisposable.dispose()
self.shareStatusDisposable?.dispose()
self.sharedOpenStoryDisposable.dispose()
self.copyProtectionTooltipController?.dismiss()
}

View File

@ -30,6 +30,7 @@ import Postbox
import FetchManagerImpl
import AnimationCache
import MultiAnimationRenderer
import AvatarNode
private enum ChatListRecentEntryStableId: Hashable {
case topPeers
@ -38,13 +39,13 @@ private enum ChatListRecentEntryStableId: Hashable {
private enum ChatListRecentEntry: Comparable, Identifiable {
case topPeers([EnginePeer], PresentationTheme, PresentationStrings)
case peer(index: Int, peer: RecentlySearchedPeer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, EngineGlobalNotificationSettings)
case peer(index: Int, peer: RecentlySearchedPeer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, EngineGlobalNotificationSettings, PeerStoryStats?)
var stableId: ChatListRecentEntryStableId {
switch self {
case .topPeers:
return .topPeers
case let .peer(_, peer, _, _, _, _, _, _):
case let .peer(_, peer, _, _, _, _, _, _, _):
return .peerId(peer.peer.peerId)
}
}
@ -66,8 +67,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
} else {
return false
}
case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsGlobalNotificationsSettings):
if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsGlobalNotificationsSettings) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsTimeFormat == rhsTimeFormat && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsGlobalNotificationsSettings == rhsGlobalNotificationsSettings {
case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsGlobalNotificationsSettings, lhsStoryStats):
if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsGlobalNotificationsSettings, rhsStoryStats) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsTimeFormat == rhsTimeFormat && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsGlobalNotificationsSettings == rhsGlobalNotificationsSettings && lhsStoryStats == rhsStoryStats {
return true
} else {
return false
@ -79,17 +80,17 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
switch lhs {
case .topPeers:
return true
case let .peer(lhsIndex, _, _, _, _, _, _, _):
case let .peer(lhsIndex, _, _, _, _, _, _, _, _):
switch rhs {
case .topPeers:
return false
case let .peer(rhsIndex, _, _, _, _, _, _, _):
case let .peer(rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
}
}
}
func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> ListViewItem {
func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?) -> 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 {
switch self {
case let .topPeers(peers, theme, strings):
return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in
@ -101,7 +102,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
gesture?.cancel()
}
})
case let .peer(_, peer, theme, strings, timeFormat, nameSortOrder, nameDisplayOrder, globalNotificationSettings):
case let .peer(_, peer, theme, strings, timeFormat, nameSortOrder, nameDisplayOrder, globalNotificationSettings, storyStats):
let primaryPeer: EnginePeer
var chatPeer: EnginePeer?
let maybeChatPeer = EnginePeer(peer.peer.peers[peer.peer.peerId]!)
@ -248,7 +249,18 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
}
},
animationCache: animationCache,
animationRenderer: animationRenderer
animationRenderer: animationRenderer,
storyStats: storyStats.flatMap { stats in
return (stats.totalCount, unseen: stats.unseenCount, stats.hasUnseenCloseFriends)
},
openStories: { itemPeer, sourceNode in
guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else {
return
}
if let sourceNode = sourceNode as? ContactsPeerItemNode {
openStories(peer.id, sourceNode.avatarNode)
}
}
)
}
}
@ -480,7 +492,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ListViewItem {
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem {
switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType):
let actionTitle: String?
@ -576,6 +588,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in
return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends)
}, openStories: { itemPeer, sourceNode in
guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else {
return
}
if let sourceNode = sourceNode as? ContactsPeerItemNode {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats):
let primaryPeer: EnginePeer
@ -669,6 +688,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in
return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends)
}, openStories: { itemPeer, sourceNode in
guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else {
return
}
if let sourceNode = sourceNode as? ContactsPeerItemNode {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats):
var enabled = true
@ -730,6 +756,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in
return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends)
}, openStories: { itemPeer, sourceNode in
guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else {
return
}
if let sourceNode = sourceNode as? ContactsPeerItemNode {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats):
let header: ChatListSearchItemHeader
@ -855,22 +888,22 @@ public struct ChatListSearchContainerTransition {
}
}
private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> ChatListSearchContainerRecentTransition {
private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer, Int64?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?) -> 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)
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, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, 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, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories), directionHint: nil) }
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
}
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition {
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
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, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
}
@ -2185,7 +2218,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openPremiumIntro: {
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, openStories: { [weak self] subject, sourceNode in
guard let self else {
return
}
guard case let .peer(id) = subject else {
return
}
if let sourceNode = sourceNode as? ChatListItemNode {
self.interaction.openStories?(id, sourceNode.avatarNode)
}
})
chatListInteraction.isSearchMode = true
@ -2293,7 +2335,58 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
})
self.searchDisposable.set((foundItems
self.searchDisposable.set((foundItems |> mapToSignal { items -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
guard let (items, isSearching) = items else {
return .single(nil)
}
var storyStatsIds: [EnginePeer.Id] = []
for item in items {
switch item {
case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _, _):
if case .user = peer {
storyStatsIds.append(peer.id)
}
case let .localPeer(peer, _, _, _, _, _, _, _, _, _):
if case .user = peer {
storyStatsIds.append(peer.id)
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _):
if foundPeer.peer is TelegramUser {
storyStatsIds.append(foundPeer.peer.id)
}
case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _):
if let peer = peer.peer, case .user = peer {
storyStatsIds.append(peer.id)
}
default:
break
}
}
return context.engine.data.subscribe(
EngineDataMap(
storyStatsIds.map(TelegramEngine.EngineData.Item.Peer.StoryStats.init(id:))
)
)
|> map { stats -> ([ChatListSearchEntry], Bool)? in
var mappedItems = items
for i in 0 ..< mappedItems.count {
switch mappedItems[i] {
case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, _):
mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil)
case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _):
mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil)
case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _):
mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil)
default:
break
}
}
return (mappedItems, isSearching)
}
}
|> deliverOnMainQueue).start(next: { [weak self] foundItems in
if let strongSelf = self {
let previousSelectedMessageIds = previousSelectedMessages.swap(strongSelf.selectedMessages)
@ -2418,6 +2511,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.context.fetchManager.toggleInteractiveFetchPaused(resourceId: entry.resourceReference.resource.id.stringRepresentation, isPaused: !allPaused)
}
})
}, openStories: { peerId, avatarNode in
strongSelf.interaction.openStories?(peerId, avatarNode)
})
strongSelf.currentEntries = newEntries
if strongSelf.key == .downloads {
@ -2451,11 +2546,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var recentItems = combineLatest(
hasRecentPeers,
fixedRecentlySearchedPeers,
fixedRecentlySearchedPeers |> mapToSignal { peers -> Signal<([RecentlySearchedPeer], [EnginePeer.Id: PeerStoryStats]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.peer.peerId).map { id in
return TelegramEngine.EngineData.Item.Peer.StoryStats(id: id)
})
)
|> map { stats -> ([RecentlySearchedPeer], [EnginePeer.Id: PeerStoryStats]) in
var mappedStats: [EnginePeer.Id: PeerStoryStats] = [:]
for (id, value) in stats {
if let value {
mappedStats[id] = value
}
}
return (peers, mappedStats)
}
},
presentationDataPromise.get(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global())
)
|> mapToSignal { hasRecentPeers, peers, presentationData, globalNotificationSettings -> Signal<[ChatListRecentEntry], NoError> in
|> mapToSignal { hasRecentPeers, peersAndStories, presentationData, globalNotificationSettings -> Signal<[ChatListRecentEntry], NoError> in
let (peers, peerStoryStats) = peersAndStories
var entries: [ChatListRecentEntry] = []
if !peersFilter.contains(.onlyGroups) {
if hasRecentPeers {
@ -2474,7 +2586,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
peerIds.insert(peer.id)
entries.append(.peer(index: index, peer: searchedPeer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalNotificationSettings))
entries.append(.peer(index: index, peer: searchedPeer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalNotificationSettings, peerStoryStats[peer.id]))
index += 1
}
}
@ -2517,7 +2629,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
interaction.clearRecentSearch()
}, deletePeer: { peerId in
let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).start()
}, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer)
}, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer, openStories: { peerId, avatarNode in
interaction.openStories?(peerId, avatarNode)
})
strongSelf.enqueueRecentTransition(transition, firstTime: firstTime)
}
}))

View File

@ -20,7 +20,6 @@ import ComponentFlow
import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
import AvatarStoryIndicatorComponent
public final class ContactItemHighlighting {
public var chatLocation: ChatLocation?
@ -400,7 +399,6 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private let offsetContainerNode: ASDisplayNode
private let avatarNodeContainer: ASDisplayNode
public let avatarNode: AvatarNode
private var avatarStoryIndicator: ComponentView<Empty>?
private var avatarIconView: ComponentHostView<Empty>?
private var avatarIconComponent: EmojiStatusComponent?
private let titleNode: TextNode
@ -414,6 +412,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var actionButtonNodes: [HighlightableButtonNode]?
private var arrowButtonNode: HighlightableButtonNode?
private var avatarTapRecognizer: UITapGestureRecognizer?
private var isHighlighted: Bool = false
private var peerPresenceManager: PeerPresenceStatusManager?
@ -1033,6 +1033,29 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
break
}
strongSelf.avatarNode.setStoryStats(
storyStats: item.storyStats.flatMap { stats in
return AvatarNode.StoryStats(
totalCount: stats.total,
unseenCount: stats.unseen,
hasUnseenCloseFriendsItems: stats.hasUnseenCloseFriends
)
},
presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: item.presentationData.theme),
lineWidth: 1.33,
inactiveLineWidth: 1.33
),
transition: animated ? Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) : .immediate
)
if strongSelf.avatarTapRecognizer == nil {
let avatarTapRecognizer = UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:)))
strongSelf.avatarTapRecognizer = avatarTapRecognizer
strongSelf.avatarNode.view.addGestureRecognizer(avatarTapRecognizer)
}
strongSelf.avatarNode.isUserInteractionEnabled = item.storyStats != nil
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
@ -1086,62 +1109,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
transition.updatePosition(node: strongSelf.avatarNodeContainer, position: avatarFrame.center)
transition.updateBounds(node: strongSelf.avatarNodeContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
var avatarScale: CGFloat = 1.0
if item.storyStats != nil {
avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width
}
let avatarScale: CGFloat = 1.0
transition.updateTransformScale(node: strongSelf.avatarNodeContainer, scale: CGPoint(x: avatarScale, y: avatarScale))
let storyIndicatorScale: CGFloat = 1.0
if let storyStats = item.storyStats {
var indicatorTransition = Transition(transition)
let avatarStoryIndicator: ComponentView<Empty>
if let current = strongSelf.avatarStoryIndicator {
avatarStoryIndicator = current
} else {
indicatorTransition = .immediate
avatarStoryIndicator = ComponentView()
strongSelf.avatarStoryIndicator = avatarStoryIndicator
}
var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 2.0, y: avatarFrame.minY + 2.0), size: CGSize(width: avatarFrame.width - 2.0 - 2.0, height: avatarFrame.height - 2.0 - 2.0))
indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5
let _ = avatarStoryIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyStats.unseen != 0,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends,
colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme),
activeLineWidth: 1.0 + UIScreenPixel,
inactiveLineWidth: 1.0 + UIScreenPixel,
counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen)
)),
environment: {},
containerSize: indicatorFrame.size
)
if let avatarStoryIndicatorView = avatarStoryIndicator.view {
if avatarStoryIndicatorView.superview == nil {
avatarStoryIndicatorView.isUserInteractionEnabled = true
avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:))))
strongSelf.offsetContainerNode.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarNodeContainer.view)
}
indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center)
indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale)
}
} else {
if let avatarStoryIndicator = strongSelf.avatarStoryIndicator {
strongSelf.avatarStoryIndicator = nil
avatarStoryIndicator.view?.removeFromSuperview()
}
}
if case let .thread(_, title, icon, color) = item.peer {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
@ -1543,7 +1514,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) {
if let result = self.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarNode.view), with: event) {
return result
}

View File

@ -121,7 +121,8 @@ public final class HashtagSearchController: TelegramBaseController {
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], requestPeerType: nil, location: .chatList(groupId: .root), key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
}, toggleExpandGlobalResults: {
}, searchPeer: { _ in
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {})
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {}, openStories: { _, _ in
})
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
}
})

View File

@ -1090,5 +1090,34 @@ public extension TelegramEngine.EngineData.Item {
return view.info?.data.get(MessageHistoryThreadData.self)
}
}
public struct StoryStats: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = PeerStoryStats?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .peerStoryStats(peerIds: Set([self.id]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PeerStoryStatsView else {
preconditionFailure()
}
if let result = view.storyStats[self.id] {
return result
} else {
return nil
}
}
}
}
}

View File

@ -1179,7 +1179,10 @@ final class PeerSelectionControllerNode: ASDisplayNode {
},
presentInGlobalOverlay: { _, _ in
},
navigationController: nil
navigationController: nil,
parentController: { [weak self] in
return self?.controller
}
), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()