mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Download list improvements
This commit is contained in:
parent
307f8a841d
commit
20748909d1
@ -501,6 +501,7 @@ public final class ContactSelectionControllerParams {
|
||||
public enum ChatListSearchFilter: Equatable {
|
||||
case chats
|
||||
case media
|
||||
case downloads
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
@ -514,14 +515,16 @@ public enum ChatListSearchFilter: Equatable {
|
||||
return 0
|
||||
case .media:
|
||||
return 1
|
||||
case .links:
|
||||
case .downloads:
|
||||
return 2
|
||||
case .files:
|
||||
case .links:
|
||||
return 3
|
||||
case .music:
|
||||
case .files:
|
||||
return 4
|
||||
case .voice:
|
||||
case .music:
|
||||
return 5
|
||||
case .voice:
|
||||
return 6
|
||||
case let .peer(peerId, _, _, _):
|
||||
return peerId.id._internalGetInt64Value()
|
||||
case let .date(_, date, _):
|
||||
@ -616,6 +619,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController
|
||||
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func openStorageUsage(context: AccountContext)
|
||||
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
|
@ -27,6 +27,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
case subscribers
|
||||
case downloading
|
||||
case recentDownloads
|
||||
|
||||
fileprivate func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
@ -74,6 +76,12 @@ public enum ChatListSearchItemHeaderType {
|
||||
return strings.ChatList_HeaderImportIntoAnExistingGroup
|
||||
case .subscribers:
|
||||
return strings.Channel_ChannelSubscribersHeader
|
||||
case .downloading:
|
||||
//TODO:localize
|
||||
return "Downloading"
|
||||
case .recentDownloads:
|
||||
//TODO:localize
|
||||
return "Recently Downloaded"
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +131,10 @@ public enum ChatListSearchItemHeaderType {
|
||||
return .orImportIntoAnExistingGroup
|
||||
case .subscribers:
|
||||
return .subscribers
|
||||
case .downloading:
|
||||
return .downloading
|
||||
case .recentDownloads:
|
||||
return .recentDownloads
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,6 +166,8 @@ private enum ChatListSearchItemHeaderId: Int32 {
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
case subscribers
|
||||
case downloading
|
||||
case recentDownloads
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
@ -110,7 +110,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
|
||||
let placeholder = self.placeholder
|
||||
|
||||
return { item, params, nextIsPinned, isEnabled in
|
||||
return { [weak self] item, params, nextIsPinned, isEnabled in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
|
||||
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
|
||||
@ -120,7 +120,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
return (layout, { animated in
|
||||
if let strongSelf = self {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
|
@ -62,6 +62,9 @@ swift_library(
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/FetchManagerImpl:FetchManagerImpl",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -25,6 +25,9 @@ import TooltipUI
|
||||
import TelegramCallsUI
|
||||
import StickerResources
|
||||
import PasswordSetupUI
|
||||
import FetchManagerImpl
|
||||
import ComponentFlow
|
||||
import LottieAnimationComponent
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
@ -151,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
|
||||
|
||||
private var activeDownloadsDisposable: Disposable?
|
||||
|
||||
private var didSetupTabs = false
|
||||
|
||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@ -467,6 +472,95 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
})
|
||||
self.searchContentNode?.updateExpansionProgress(0.0)
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
|
||||
enum State {
|
||||
case empty
|
||||
case downloading
|
||||
case hasUnseen
|
||||
}
|
||||
|
||||
var stateSignal: Signal<State, NoError> = (combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox))
|
||||
|> map { entries, recentDownloadItems -> State in
|
||||
if !entries.isEmpty {
|
||||
return .downloading
|
||||
} else {
|
||||
for item in recentDownloadItems {
|
||||
if !item.isSeen {
|
||||
return .hasUnseen
|
||||
}
|
||||
}
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
|> mapToSignal { value -> Signal<State, NoError> in
|
||||
return .single(value) |> delay(0.1, queue: .mainQueue())
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue)
|
||||
|
||||
if !"".isEmpty {
|
||||
stateSignal = Signal<State, NoError>.single(.downloading)
|
||||
|> then(Signal<State, NoError>.single(.hasUnseen) |> delay(3.0, queue: .mainQueue()))
|
||||
}
|
||||
|
||||
self.activeDownloadsDisposable = stateSignal.start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch state {
|
||||
case .downloading:
|
||||
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
|
||||
content: AnyComponent(LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.Animation(
|
||||
name: "anim_search_downloading",
|
||||
colors: [
|
||||
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
],
|
||||
loop: true
|
||||
),
|
||||
size: CGSize(width: 24.0, height: 24.0)
|
||||
)),
|
||||
insets: UIEdgeInsets(),
|
||||
action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activateSearch(filter: .downloads, query: nil)
|
||||
}
|
||||
)))
|
||||
case .hasUnseen:
|
||||
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
|
||||
content: AnyComponent(LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.Animation(
|
||||
name: "anim_search_downloaded",
|
||||
colors: [
|
||||
"Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
|
||||
],
|
||||
loop: false
|
||||
),
|
||||
size: CGSize(width: 24.0, height: 24.0)
|
||||
)),
|
||||
insets: UIEdgeInsets(),
|
||||
action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activateSearch(filter: .downloads, query: nil)
|
||||
}
|
||||
)))
|
||||
case .empty:
|
||||
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if enableDebugActions {
|
||||
@ -514,6 +608,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.stateDisposable.dispose()
|
||||
self.filterDisposable.dispose()
|
||||
self.featuredFiltersDisposable.dispose()
|
||||
self.activeDownloadsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
|
@ -29,6 +29,7 @@ import ChatInterfaceState
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import TextFormat
|
||||
import Postbox
|
||||
|
||||
private enum ChatListTokenId: Int32 {
|
||||
case archive
|
||||
@ -45,14 +46,14 @@ final class ChatListSearchInteraction {
|
||||
let clearRecentSearch: () -> Void
|
||||
let addContact: (String) -> Void
|
||||
let toggleMessageSelection: (EngineMessage.Id, Bool) -> Void
|
||||
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
|
||||
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)
|
||||
let mediaMessageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
|
||||
let peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let dismissInput: () -> Void
|
||||
let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
|
||||
|
||||
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, 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?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
|
||||
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, 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, String?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
|
||||
self.openPeer = openPeer
|
||||
self.openDisabledPeer = openDisabledPeer
|
||||
self.openMessage = openMessage
|
||||
@ -112,8 +113,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
private var stateValue = ChatListSearchContainerNodeSearchState()
|
||||
private let statePromise = ValuePromise<ChatListSearchContainerNodeSearchState>()
|
||||
|
||||
private var selectedFilterKey: ChatListSearchFilterEntryId?
|
||||
private var selectedFilterKeyPromise = Promise<ChatListSearchFilterEntryId?>()
|
||||
private var selectedFilter: ChatListSearchFilterEntry?
|
||||
private var selectedFilterPromise = Promise<ChatListSearchFilterEntry?>()
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
|
||||
private weak var copyProtectionTooltipController: TooltipController?
|
||||
@ -134,8 +135,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
self.navigationController = navigationController
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.selectedFilterKey = .filter(initialFilter.id)
|
||||
self.selectedFilterKeyPromise.set(.single(self.selectedFilterKey))
|
||||
self.selectedFilter = .filter(initialFilter)
|
||||
self.selectedFilterPromise.set(.single(self.selectedFilter))
|
||||
|
||||
self.openMessage = originalOpenMessage
|
||||
self.present = present
|
||||
@ -217,8 +218,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
return state.withUpdatedSelectedMessageIds(selectedMessageIds)
|
||||
}
|
||||
}
|
||||
}, messageContextAction: { [weak self] message, node, rect, gesture in
|
||||
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture)
|
||||
}, messageContextAction: { [weak self] message, node, rect, gesture, paneKey, downloadResourceId in
|
||||
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture, paneKey: paneKey, downloadResourceId: downloadResourceId)
|
||||
}, mediaMessageContextAction: { [weak self] message, node, rect, gesture in
|
||||
self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture)
|
||||
}, peerContextAction: { peer, source, node, gesture in
|
||||
@ -244,6 +245,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
filterKey = .chats
|
||||
case .media:
|
||||
filterKey = .media
|
||||
case .downloads:
|
||||
filterKey = .downloads
|
||||
case .links:
|
||||
filterKey = .links
|
||||
case .files:
|
||||
@ -253,8 +256,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
case .voice:
|
||||
filterKey = .voice
|
||||
}
|
||||
strongSelf.selectedFilterKey = .filter(filterKey.id)
|
||||
strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey))
|
||||
strongSelf.selectedFilter = .filter(filterKey)
|
||||
strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter))
|
||||
strongSelf.transitionFraction = transitionFraction
|
||||
|
||||
if let (layout, _) = strongSelf.validLayout {
|
||||
@ -262,9 +265,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
|
||||
filters = suggestedFilters
|
||||
} else {
|
||||
filters = [.chats, .media, .links, .files, .music, .voice]
|
||||
filters = [.chats, .media, .downloads, .links, .files, .music, .voice]
|
||||
}
|
||||
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
|
||||
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,6 +286,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .chats
|
||||
case .media:
|
||||
key = .media
|
||||
case .downloads:
|
||||
key = .downloads
|
||||
case .links:
|
||||
key = .links
|
||||
case .files:
|
||||
@ -306,7 +311,31 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
|
||||
self.filterContainerNode.filterPressed?(initialFilter)
|
||||
|
||||
let suggestedPeers = self.searchQuery.get()
|
||||
let searchQuerySignal = self.searchQuery.get()
|
||||
|
||||
let suggestedPeers = self.selectedFilterPromise.get()
|
||||
|> map { filter -> Bool in
|
||||
guard let filter = filter else {
|
||||
return false
|
||||
}
|
||||
switch filter {
|
||||
case let .filter(filter):
|
||||
switch filter {
|
||||
case .downloads:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { value -> Signal<String?, NoError> in
|
||||
if value {
|
||||
return searchQuerySignal
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { query -> Signal<[EnginePeer], NoError> in
|
||||
if let query = query {
|
||||
return context.account.postbox.searchPeers(query: query.lowercased())
|
||||
@ -321,13 +350,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||
|> take(1)
|
||||
|
||||
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterKeyPromise.get(), self.searchQuery.get(), accountPeer)
|
||||
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer)
|
||||
|> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in
|
||||
if searchQuery?.isEmpty ?? true {
|
||||
return .single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer)))
|
||||
return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))
|
||||
} else {
|
||||
return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|
||||
|> then(.single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer))))
|
||||
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))))
|
||||
}
|
||||
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in
|
||||
var suggestedFilters: [ChatListSearchFilter] = []
|
||||
@ -523,6 +552,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .music
|
||||
case .voice:
|
||||
key = .voice
|
||||
case .downloads:
|
||||
key = .downloads
|
||||
default:
|
||||
key = .chats
|
||||
}
|
||||
@ -553,7 +584,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}
|
||||
|
||||
let overflowInset: CGFloat = 20.0
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
||||
var bottomIntrinsicInset = layout.intrinsicInsets.bottom
|
||||
if case .root = self.groupId {
|
||||
@ -737,10 +768,137 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
let _ = self.paneContainerNode.scrollToTop()
|
||||
}
|
||||
|
||||
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) {
|
||||
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?, paneKey: ChatListSearchPaneKey, downloadResourceId: String?) {
|
||||
guard let node = node as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||||
|
||||
if paneKey == .downloads {
|
||||
let isCachedValue: Signal<Bool, NoError>
|
||||
if let downloadResourceId = downloadResourceId {
|
||||
isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResourceId), resourceSize: nil)
|
||||
|> map { status -> Bool in
|
||||
switch status {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
isCachedValue = .single(false)
|
||||
}
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError> = Signal { subscriber in
|
||||
subscriber.putNext(false)
|
||||
let previous = Atomic<Bool?>(value: nil)
|
||||
return isCachedValue.start(next: { value in
|
||||
let previousSwapped = previous.swap(value)
|
||||
if let previousSwapped = previousSwapped, previousSwapped != value {
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let items = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: [message.id: message], peers: [:]),
|
||||
isCachedValue |> take(1)
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] actions, isCachedValue -> [ContextMenuItem] in
|
||||
guard let strongSelf = self else {
|
||||
return []
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if isCachedValue {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Delete from Cache", textColor: .primary, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self, let downloadResourceId = downloadResourceId else {
|
||||
f(.default)
|
||||
return
|
||||
}
|
||||
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources([MediaResourceId(downloadResourceId)], notify: true)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
f(.dismissWithoutContent)
|
||||
})
|
||||
})))
|
||||
} else {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Cancel Downloading", textColor: .primary, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self, let downloadResourceId = downloadResourceId else {
|
||||
f(.default)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = strongSelf
|
||||
let _ = downloadResourceId
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false)
|
||||
})
|
||||
})))
|
||||
|
||||
if !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if actions.options.contains(.deleteLocally) {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { controller, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forLocalPeer).start()
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
|
||||
if actions.options.contains(.deleteGlobally) {
|
||||
let text: String
|
||||
if let mainPeer = message.peers[message.id.peerId] {
|
||||
if mainPeer is TelegramUser {
|
||||
text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(mainPeer).compactDisplayTitle).string
|
||||
} else {
|
||||
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
} else {
|
||||
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { controller, f in
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).start()
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node, shouldBeDismissed: shouldBeDismissed)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture)
|
||||
self.presentInGlobalOverlay?(controller, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let _ = storedMessageFromSearch(account: self.context.account, message: message._asMessage()).start()
|
||||
|
||||
var linkForCopying: String?
|
||||
@ -756,8 +914,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}
|
||||
}
|
||||
|
||||
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||||
|
||||
let context = self.context
|
||||
let (peers, messages) = self.currentMessages
|
||||
let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers)
|
||||
@ -1150,10 +1306,13 @@ private final class MessageContextExtractedContentSource: ContextExtractedConten
|
||||
let ignoreContentTouches: Bool = true
|
||||
let blurBackground: Bool = true
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError>
|
||||
|
||||
private let sourceNode: ContextExtractedContentContainingNode
|
||||
|
||||
init(sourceNode: ContextExtractedContentContainingNode) {
|
||||
init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal<Bool, NoError>? = nil) {
|
||||
self.sourceNode = sourceNode
|
||||
self.shouldBeDismissed = shouldBeDismissed ?? .single(false)
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
|
@ -84,6 +84,10 @@ private final class ItemNode: ASDisplayNode {
|
||||
case .media:
|
||||
title = presentationData.strings.ChatList_Search_FilterMedia
|
||||
icon = nil
|
||||
case .downloads:
|
||||
//TODO:localize
|
||||
title = "Downloads"
|
||||
icon = nil
|
||||
case .links:
|
||||
title = presentationData.strings.ChatList_Search_FilterLinks
|
||||
icon = nil
|
||||
|
@ -26,6 +26,8 @@ import AppBundle
|
||||
import ShimmerEffect
|
||||
import ChatListSearchRecentPeersNode
|
||||
import UndoUI
|
||||
import Postbox
|
||||
import FetchManagerImpl
|
||||
|
||||
private enum ChatListRecentEntryStableId: Hashable {
|
||||
case topPeers
|
||||
@ -217,7 +219,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
public enum ChatListSearchEntryStableId: Hashable {
|
||||
case localPeerId(EnginePeer.Id)
|
||||
case globalPeerId(EnginePeer.Id)
|
||||
case messageId(EngineMessage.Id)
|
||||
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
|
||||
case addContact
|
||||
}
|
||||
|
||||
@ -228,9 +230,54 @@ public enum ChatListSearchSectionExpandType {
|
||||
}
|
||||
|
||||
public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
public enum MessageOrderingKey: Comparable {
|
||||
case index(MessageIndex)
|
||||
case downloading(FetchManagerPriorityKey)
|
||||
case downloaded(timestamp: Int32, index: MessageIndex)
|
||||
|
||||
public static func <(lhs: MessageOrderingKey, rhs: MessageOrderingKey) -> Bool {
|
||||
switch lhs {
|
||||
case let .index(lhsIndex):
|
||||
if case let .index(rhsIndex) = rhs {
|
||||
return lhsIndex > rhsIndex
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case let .downloading(lhsKey):
|
||||
switch rhs {
|
||||
case let .downloading(rhsKey):
|
||||
return lhsKey > rhsKey
|
||||
case .index:
|
||||
return false
|
||||
case .downloaded:
|
||||
return true
|
||||
}
|
||||
case let .downloaded(lhsTimestamp, lhsIndex):
|
||||
switch rhs {
|
||||
case let .downloaded(rhsTimestamp, rhsIndex):
|
||||
if lhsTimestamp != rhsTimestamp {
|
||||
return lhsTimestamp > rhsTimestamp
|
||||
} else {
|
||||
return lhsIndex > rhsIndex
|
||||
}
|
||||
case .downloading:
|
||||
return false
|
||||
case .index:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageSection: Hashable {
|
||||
case generic
|
||||
case downloading
|
||||
case recentlyDownloaded
|
||||
}
|
||||
|
||||
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
|
||||
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
|
||||
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool)
|
||||
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, String?, MessageSection)
|
||||
case addContact(String, PresentationTheme, PresentationStrings)
|
||||
|
||||
public var stableId: ChatListSearchEntryStableId {
|
||||
@ -239,8 +286,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
return .localPeerId(peer.id)
|
||||
case let .globalPeer(peer, _, _, _, _, _, _, _):
|
||||
return .globalPeerId(peer.peer.id)
|
||||
case let .message(message, _, _, _, _, _, _):
|
||||
return .messageId(message.id)
|
||||
case let .message(message, _, _, _, _, _, _, _, _, section):
|
||||
return .messageId(message.id, section)
|
||||
case .addContact:
|
||||
return .addContact
|
||||
}
|
||||
@ -260,8 +307,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader):
|
||||
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = rhs {
|
||||
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection):
|
||||
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection) = rhs {
|
||||
if lhsMessage.id != rhsMessage.id {
|
||||
return false
|
||||
}
|
||||
@ -286,6 +333,15 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
if lhsDisplayCustomHeader != rhsDisplayCustomHeader {
|
||||
return false
|
||||
}
|
||||
if lhsKey != rhsKey {
|
||||
return false
|
||||
}
|
||||
if lhsResourceId != rhsResourceId {
|
||||
return false
|
||||
}
|
||||
if lhsSection != rhsSection {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -325,9 +381,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .message, .addContact:
|
||||
return true
|
||||
}
|
||||
case let .message(lhsMessage, _, _, _, _, _, _):
|
||||
if case let .message(rhsMessage, _, _, _, _, _, _) = rhs {
|
||||
return lhsMessage.index < rhsMessage.index
|
||||
case let .message(_, _, _, _, _, _, _, lhsKey, _, _):
|
||||
if case let .message(_, _, _, _, _, _, _, rhsKey, _, _) = rhs {
|
||||
return lhsKey < rhsKey
|
||||
} else if case .addContact = rhs {
|
||||
return true
|
||||
} else {
|
||||
@ -338,7 +394,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem {
|
||||
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)?, openStorageSettings: @escaping () -> Void) -> ListViewItem {
|
||||
switch self {
|
||||
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType):
|
||||
let primaryPeer: EnginePeer
|
||||
@ -486,11 +542,29 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture)
|
||||
}
|
||||
})
|
||||
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader):
|
||||
let header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _):
|
||||
let header: ChatListSearchItemHeader
|
||||
switch orderingKey {
|
||||
case .downloading:
|
||||
//TODO:localize
|
||||
header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Pause All", action: {})
|
||||
case .downloaded:
|
||||
//TODO:localize
|
||||
header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Settings", action: {
|
||||
openStorageSettings()
|
||||
})
|
||||
case .index:
|
||||
header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
|
||||
var isMedia = false
|
||||
if let tagMask = tagMask, tagMask != .photoOrVideo {
|
||||
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true)
|
||||
isMedia = true
|
||||
} else if key == .downloads {
|
||||
isMedia = true
|
||||
}
|
||||
if isMedia {
|
||||
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: key == .downloads ? header : nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: key != .downloads, isDownloadList: key == .downloads)
|
||||
} else {
|
||||
return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
@ -540,12 +614,12 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [
|
||||
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, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> 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, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)?, openStorageSettings: @escaping () -> 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, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), 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, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), 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, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings), 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, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings), directionHint: nil) }
|
||||
|
||||
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
|
||||
}
|
||||
@ -617,6 +691,25 @@ public struct ChatListSearchOptions {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadItem: Equatable {
|
||||
let resourceId: MediaResourceId
|
||||
let message: Message
|
||||
let priority: FetchManagerPriorityKey
|
||||
|
||||
static func ==(lhs: DownloadItem, rhs: DownloadItem) -> Bool {
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.priority != rhs.priority {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
private let context: AccountContext
|
||||
private let interaction: ChatListSearchInteraction
|
||||
@ -704,6 +797,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
tagMask = nil
|
||||
case .media:
|
||||
tagMask = .photoOrVideo
|
||||
case .downloads:
|
||||
tagMask = nil
|
||||
case .links:
|
||||
tagMask = .webPage
|
||||
case .files:
|
||||
@ -811,13 +906,120 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
let presentationDataPromise = self.presentationDataPromise
|
||||
let searchStatePromise = self.searchStatePromise
|
||||
let selectionPromise = self.selectedMessagesPromise
|
||||
let foundItems = combineLatest(searchQuery, searchOptions)
|
||||
|> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
|
||||
if query == nil && options == nil && tagMask == nil {
|
||||
|
||||
let downloadItems: Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError>
|
||||
if key == .downloads {
|
||||
downloadItems = combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox))
|
||||
|> mapToSignal { entries, recentDownloadItems -> Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError> in
|
||||
var itemSignals: [Signal<DownloadItem?, NoError>] = []
|
||||
|
||||
for entry in entries {
|
||||
switch entry.id.locationKey {
|
||||
case let .messageId(id):
|
||||
itemSignals.append(context.account.postbox.transaction { transaction -> DownloadItem? in
|
||||
if let message = transaction.getMessage(id) {
|
||||
return DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), itemSignals)
|
||||
|> map { items -> (inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]) in
|
||||
return (items.compactMap { $0 }, recentDownloadItems)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downloadItems = .single(([], []))
|
||||
}
|
||||
|
||||
let foundItems = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems)
|
||||
|> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
|
||||
if query == nil && options == nil && key == .chats {
|
||||
let _ = currentRemotePeers.swap(nil)
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
if key == .downloads {
|
||||
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
|
||||
|
||||
func messageMatchesTokens(message: EngineMessage, tokens: [ValueBoxKey]) -> Bool {
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if let fileName = file.fileName {
|
||||
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaImage {
|
||||
if matchStringIndexTokens(stringIndexTokens("Photo Image", transliteration: .none), with: tokens) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return presentationDataPromise.get()
|
||||
|> map { presentationData -> ([ChatListSearchEntry], Bool)? in
|
||||
var entries: [ChatListSearchEntry] = []
|
||||
var existingMessageIds = Set<MessageId>()
|
||||
for item in downloadItems.inProgressItems {
|
||||
if existingMessageIds.contains(item.message.id) {
|
||||
continue
|
||||
}
|
||||
existingMessageIds.insert(item.message.id)
|
||||
|
||||
let message = EngineMessage(item.message)
|
||||
|
||||
if !queryTokens.isEmpty {
|
||||
if !messageMatchesTokens(message: message, tokens: queryTokens) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var peer = EngineRenderedPeer(message: message)
|
||||
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
|
||||
if let channelPeer = message.peers[migrationReference.peerId] {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloading(item.priority), item.resourceId.stringRepresentation, .downloading))
|
||||
}
|
||||
for item in downloadItems.doneItems {
|
||||
if !item.isSeen {
|
||||
Queue.mainQueue().async {
|
||||
self?.scheduleMarkRecentDownloadsAsSeen()
|
||||
}
|
||||
}
|
||||
if existingMessageIds.contains(item.message.id) {
|
||||
continue
|
||||
}
|
||||
existingMessageIds.insert(item.message.id)
|
||||
|
||||
let message = EngineMessage(item.message)
|
||||
|
||||
if !queryTokens.isEmpty {
|
||||
if !messageMatchesTokens(message: message, tokens: queryTokens) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var peer = EngineRenderedPeer(message: message)
|
||||
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
|
||||
if let channelPeer = message.peers[migrationReference.peerId] {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloaded(timestamp: item.timestamp, index: message.index), item.resourceId, .recentlyDownloaded))
|
||||
}
|
||||
return (entries.sorted(), false)
|
||||
}
|
||||
}
|
||||
|
||||
let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1)
|
||||
let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError>
|
||||
if let query = query {
|
||||
@ -1108,7 +1310,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, true))
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic))
|
||||
index += 1
|
||||
}
|
||||
|
||||
@ -1131,7 +1333,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId))
|
||||
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@ -1249,8 +1451,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
})
|
||||
|
||||
let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
interaction.dismissInput()
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
|
||||
|
||||
let gallerySource: GalleryControllerItemSource
|
||||
|
||||
if strongSelf.key == .downloads {
|
||||
gallerySource = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil))
|
||||
} else {
|
||||
gallerySource = .custom(messages: foundMessages |> map { message, a, b in
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, messageId: message.id, loadMore: {
|
||||
loadMore()
|
||||
})
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
|
||||
interaction.dismissInput()
|
||||
}, present: { c, a in
|
||||
interaction.present(c, a)
|
||||
@ -1277,13 +1495,25 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, at: message.id, loadMore: {
|
||||
loadMore()
|
||||
}), gallerySource: .custom(messages: foundMessages |> map { message, a, b in
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, messageId: message.id, loadMore: {
|
||||
loadMore()
|
||||
})))
|
||||
}, openMessageContextMenu: { message, _, node, rect, gesture in
|
||||
interaction.messageContextAction(EngineMessage(message), node, rect, gesture)
|
||||
}), gallerySource: gallerySource))
|
||||
}, openMessageContextMenu: { [weak self] message, _, node, rect, gesture in
|
||||
guard let strongSelf = self, let currentEntries = strongSelf.currentEntries else {
|
||||
return
|
||||
}
|
||||
|
||||
var fetchResourceId: String?
|
||||
for entry in currentEntries {
|
||||
switch entry {
|
||||
case let .message(m, _, _, _, _, _, _, _, resourceId, _):
|
||||
if m.id == message.id {
|
||||
fetchResourceId = resourceId
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
interaction.messageContextAction(EngineMessage(message), node, rect, gesture, key, fetchResourceId)
|
||||
}, toggleMessagesSelection: { messageId, selected in
|
||||
if let messageId = messageId.first {
|
||||
interaction.toggleMessageSelection(messageId, selected)
|
||||
@ -1355,7 +1585,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
|
||||
interaction.peerContextAction?(message, node, rect, gesture)
|
||||
}, toggleExpandLocalResults: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -1376,15 +1606,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
return state
|
||||
}
|
||||
}, searchPeer: { peer in
|
||||
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in
|
||||
interaction.messageContextAction(message, node, rect, gesture)
|
||||
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResourceId in
|
||||
interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResourceId)
|
||||
}, openStorageSettings: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context)
|
||||
})
|
||||
strongSelf.currentEntries = newEntries
|
||||
strongSelf.enqueueTransition(transition, firstTime: firstTime)
|
||||
|
||||
var messages: [EngineMessage] = []
|
||||
for entry in newEntries {
|
||||
if case let .message(message, _, _, _, _, _, _) = entry {
|
||||
if case let .message(message, _, _, _, _, _, _, _, _, _) = entry {
|
||||
messages.append(message)
|
||||
}
|
||||
}
|
||||
@ -1652,6 +1887,27 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
func didBecomeFocused() {
|
||||
if self.key == .downloads {
|
||||
self.scheduleMarkRecentDownloadsAsSeen()
|
||||
}
|
||||
}
|
||||
|
||||
private var scheduledMarkRecentDownloadsAsSeen: Bool = false
|
||||
|
||||
func scheduleMarkRecentDownloadsAsSeen() {
|
||||
if !self.scheduledMarkRecentDownloadsAsSeen {
|
||||
self.scheduledMarkRecentDownloadsAsSeen = true
|
||||
Queue.mainQueue().after(0.1, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.scheduledMarkRecentDownloadsAsSeen = false
|
||||
let _ = markAllRecentDownloadItemsAsSeen(postbox: strongSelf.context.account.postbox).start()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToTop() -> Bool {
|
||||
if !self.mediaNode.isHidden {
|
||||
return self.mediaNode.scrollToTop()
|
||||
@ -2110,16 +2366,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
|
||||
if strongSelf.key == .downloads {
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = true
|
||||
strongSelf.emptyResultsTitleNode.isHidden = true
|
||||
strongSelf.emptyResultsTextNode.isHidden = true
|
||||
strongSelf.emptyResultsAnimationNode.visibility = false
|
||||
} else {
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
|
||||
}
|
||||
|
||||
let displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
|
||||
var displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
|
||||
if strongSelf.key == .downloads {
|
||||
displayPlaceholder = false
|
||||
}
|
||||
|
||||
let targetAlpha: CGFloat = displayPlaceholder ? 1.0 : 0.0
|
||||
if strongSelf.shimmerNode.alpha != targetAlpha {
|
||||
let transition: ContainedViewLayoutTransition = displayPlaceholder ? .immediate : .animated(duration: 0.2, curve: .linear)
|
||||
let transition: ContainedViewLayoutTransition = (displayPlaceholder || isFirstTime) ? .immediate : .animated(duration: 0.2, curve: .linear)
|
||||
transition.updateAlpha(node: strongSelf.shimmerNode, alpha: targetAlpha, delay: 0.1)
|
||||
}
|
||||
|
||||
@ -2334,7 +2600,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
|
||||
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
|
||||
switch key {
|
||||
case .chats:
|
||||
case .chats, .downloads:
|
||||
let message = EngineMessage(
|
||||
stableId: 0,
|
||||
stableVersion: 0,
|
||||
@ -2358,7 +2624,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
associatedMessageIds: []
|
||||
)
|
||||
let readState = EnginePeerReadCounters()
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
case .media:
|
||||
return nil
|
||||
case .links:
|
||||
|
@ -702,7 +702,7 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var index: UInt32 = 0
|
||||
if let entries = entries {
|
||||
for entry in entries {
|
||||
if case let .message(message, _, _, _, _, _, _) = entry {
|
||||
if case let .message(message, _, _, _, _, _, _, _, _, _) = entry {
|
||||
self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil))
|
||||
}
|
||||
index += 1
|
||||
|
@ -19,6 +19,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode {
|
||||
func updateHiddenMedia()
|
||||
func updateSelectedMessages(animated: Bool)
|
||||
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
|
||||
func didBecomeFocused()
|
||||
var searchCurrentMessages: [EngineMessage]? { get }
|
||||
}
|
||||
|
||||
@ -44,16 +45,17 @@ final class ChatListSearchPaneWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListSearchPaneKey {
|
||||
public enum ChatListSearchPaneKey {
|
||||
case chats
|
||||
case media
|
||||
case downloads
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
case voice
|
||||
}
|
||||
|
||||
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice]
|
||||
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice]
|
||||
|
||||
struct ChatListSearchPaneSpecifier: Equatable {
|
||||
var key: ChatListSearchPaneKey
|
||||
@ -475,6 +477,9 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
|
||||
})
|
||||
}
|
||||
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
||||
if paneWasAdded && key == self.currentPaneKey {
|
||||
pane.node.didBecomeFocused()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
20
submodules/Components/LottieAnimationComponent/BUILD
Normal file
20
submodules/Components/LottieAnimationComponent/BUILD
Normal file
@ -0,0 +1,20 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "LottieAnimationComponent",
|
||||
module_name = "LottieAnimationComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
import ComponentFlow
|
||||
import Lottie
|
||||
import AppBundle
|
||||
|
||||
public final class LottieAnimationComponent: Component {
|
||||
public struct Animation: Equatable {
|
||||
public var name: String
|
||||
public var loop: Bool
|
||||
public var colors: [String: UIColor]
|
||||
|
||||
public init(name: String, colors: [String: UIColor], loop: Bool) {
|
||||
self.name = name
|
||||
self.colors = colors
|
||||
self.loop = loop
|
||||
}
|
||||
}
|
||||
|
||||
public let animation: Animation
|
||||
public let size: CGSize
|
||||
|
||||
public init(animation: Animation, size: CGSize) {
|
||||
self.animation = animation
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
|
||||
if lhs.animation != rhs.animation {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var currentAnimation: Animation?
|
||||
|
||||
private var colorCallbacks: [LOTColorValueCallback] = []
|
||||
private var animationView: LOTAnimationView?
|
||||
|
||||
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let size = CGSize(width: min(component.size.width, availableSize.width), height: min(component.size.height, availableSize.height))
|
||||
|
||||
if self.currentAnimation != component.animation {
|
||||
if let animationView = self.animationView, animationView.isAnimationPlaying {
|
||||
animationView.completionBlock = { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.update(component: component, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
animationView.loopAnimation = false
|
||||
} else {
|
||||
self.currentAnimation = component.animation
|
||||
|
||||
self.animationView?.removeFromSuperview()
|
||||
|
||||
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) {
|
||||
let view = LOTAnimationView(model: composition, in: getAppBundle())
|
||||
view.loopAnimation = component.animation.loop
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
|
||||
view.logHierarchyKeypaths()
|
||||
|
||||
for (key, value) in component.animation.colors {
|
||||
let colorCallback = LOTColorValueCallback(color: value.cgColor)
|
||||
self.colorCallbacks.append(colorCallback)
|
||||
view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))
|
||||
}
|
||||
|
||||
self.animationView = view
|
||||
self.addSubview(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let animationView = self.animationView {
|
||||
animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - component.size.width) / 2.0), y: floor((size.height - component.size.height) / 2.0)), size: component.size)
|
||||
|
||||
if !animationView.isAnimationPlaying {
|
||||
animationView.play { _ in
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ private final class FetchManagerLocationEntry {
|
||||
let ranges = Bag<IndexSet>()
|
||||
var elevatedPriorityReferenceCount: Int32 = 0
|
||||
var userInitiatedPriorityIndices: [Int32] = []
|
||||
var isPaused: Bool = false
|
||||
|
||||
var combinedRanges: IndexSet {
|
||||
var result = IndexSet()
|
||||
@ -116,7 +117,7 @@ private final class FetchManagerCategoryContext {
|
||||
private let postbox: Postbox
|
||||
private let storeManager: DownloadedMediaStoreManager?
|
||||
private let entryCompleted: (FetchManagerLocationEntryId) -> Void
|
||||
private let activeEntriesUpdated: ([FetchManagerEntrySummary]) -> Void
|
||||
private let activeEntriesUpdated: () -> Void
|
||||
|
||||
private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)?
|
||||
private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:]
|
||||
@ -133,13 +134,17 @@ private final class FetchManagerCategoryContext {
|
||||
return false
|
||||
}
|
||||
|
||||
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping ([FetchManagerEntrySummary]) -> Void) {
|
||||
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping () -> Void) {
|
||||
self.postbox = postbox
|
||||
self.storeManager = storeManager
|
||||
self.entryCompleted = entryCompleted
|
||||
self.activeEntriesUpdated = activeEntriesUpdated
|
||||
}
|
||||
|
||||
func getActiveEntries() -> [FetchManagerLocationEntry] {
|
||||
return Array(self.entries.values)
|
||||
}
|
||||
|
||||
func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (AnyMediaReference?, MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) {
|
||||
let entry: FetchManagerLocationEntry
|
||||
let previousPriorityKey: FetchManagerPriorityKey?
|
||||
@ -221,8 +226,13 @@ private final class FetchManagerCategoryContext {
|
||||
parsedRanges = resultRanges
|
||||
}
|
||||
activeContext.disposable?.dispose()
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
let postbox = self.postbox
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
|
||||
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
|
||||
}
|
||||
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> castError(FetchResourceError.self)
|
||||
@ -286,7 +296,7 @@ private final class FetchManagerCategoryContext {
|
||||
}
|
||||
}
|
||||
|
||||
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
|
||||
self.activeEntriesUpdated()
|
||||
}
|
||||
|
||||
func maybeFindAndActivateNewTopEntry() -> Bool {
|
||||
@ -366,8 +376,13 @@ private final class FetchManagerCategoryContext {
|
||||
})
|
||||
} else if ranges.isEmpty {
|
||||
} else {
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
let postbox = self.postbox
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
|
||||
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
|
||||
}
|
||||
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> castError(FetchResourceError.self)
|
||||
@ -446,7 +461,7 @@ private final class FetchManagerCategoryContext {
|
||||
activeContextsUpdated = true
|
||||
}
|
||||
|
||||
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
|
||||
self.activeEntriesUpdated()
|
||||
}
|
||||
|
||||
func withFetchStatusContext(_ id: FetchManagerLocationEntryId, _ f: (FetchManagerStatusContext) -> Void) {
|
||||
@ -493,6 +508,37 @@ private extension FetchManagerEntrySummary {
|
||||
}
|
||||
}
|
||||
|
||||
private func filterDownloadStatsEntry(entry: FetchManagerLocationEntry) -> Bool {
|
||||
guard let mediaReference = entry.mediaReference else {
|
||||
return false
|
||||
}
|
||||
if !entry.userInitiated {
|
||||
return false
|
||||
}
|
||||
switch mediaReference {
|
||||
case let .message(_, media):
|
||||
switch media {
|
||||
case let file as TelegramMediaFile:
|
||||
if file.isVideo {
|
||||
if file.isAnimated {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if file.isSticker {
|
||||
return false
|
||||
}
|
||||
if file.isVoice {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public final class FetchManagerImpl: FetchManager {
|
||||
public let queue = Queue.mainQueue()
|
||||
private let postbox: Postbox
|
||||
@ -545,21 +591,26 @@ public final class FetchManagerImpl: FetchManager {
|
||||
context.cancelEntry(id, isCompleted: true)
|
||||
})
|
||||
}
|
||||
}, activeEntriesUpdated: { [weak self] entries in
|
||||
}, activeEntriesUpdated: { [weak self] in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var hasActiveUserInitiatedEntries = false
|
||||
var activeEntries: [FetchManagerEntrySummary] = []
|
||||
for (_, context) in strongSelf.categoryContexts {
|
||||
if context.hasActiveUserInitiatedEntries {
|
||||
hasActiveUserInitiatedEntries = true
|
||||
break
|
||||
}
|
||||
activeEntries.append(contentsOf: context.getActiveEntries().filter { entry in
|
||||
return filterDownloadStatsEntry(entry: entry)
|
||||
}.compactMap { entry -> FetchManagerEntrySummary? in
|
||||
return FetchManagerEntrySummary(entry: entry)
|
||||
})
|
||||
}
|
||||
strongSelf.hasUserInitiatedEntriesValue.set(hasActiveUserInitiatedEntries)
|
||||
|
||||
strongSelf.entriesSummaryValue.set(entries)
|
||||
strongSelf.entriesSummaryValue.set(activeEntries.sorted(by: { $0.priority < $1.priority }))
|
||||
}
|
||||
})
|
||||
self.categoryContexts[key] = context
|
||||
|
@ -45,7 +45,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
|> map { result, presentationData in
|
||||
let result = result.0
|
||||
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false) })
|
||||
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic) })
|
||||
}
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {
|
||||
}, peerSelected: { _, _, _ in
|
||||
@ -95,10 +95,10 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
})
|
||||
|
||||
let firstTime = previousEntries == nil
|
||||
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: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
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: [], key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
}, toggleExpandGlobalResults: {
|
||||
}, searchPeer: { _ in
|
||||
}, searchQuery: "", searchOptions: nil, messageContextAction: nil)
|
||||
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {})
|
||||
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
})
|
||||
|
@ -419,7 +419,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
var descriptionString: String
|
||||
if let performer = performer {
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
descriptionString = performer
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||
@ -430,7 +430,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
descriptionString = ""
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -474,7 +474,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
authorName = " "
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
authorName = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
}
|
||||
|
||||
@ -482,13 +482,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
var descriptionString: String = ""
|
||||
if let duration = file.duration {
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
descriptionString = stringForDuration(Int32(duration))
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = dateString
|
||||
}
|
||||
}
|
||||
@ -496,7 +496,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
iconImage = .roundVideo(file)
|
||||
} else if !isAudio {
|
||||
let fileName: String = file.fileName ?? ""
|
||||
let fileName: String = file.fileName ?? "File"
|
||||
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
var fileExtension: String?
|
||||
@ -516,18 +516,18 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
var descriptionString: String = ""
|
||||
if let size = file.size {
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
|
||||
} else {
|
||||
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = "\(dateString)"
|
||||
}
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -554,11 +554,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
|
||||
var descriptionString: String = ""
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = "\(dateString)"
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -616,7 +616,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
if statusUpdated {
|
||||
if let file = selectedMedia as? TelegramMediaFile {
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||
if case .Fetching = value.fetchStatus {
|
||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
@ -639,10 +639,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
if isVoice {
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
}
|
||||
} else if let image = selectedMedia as? TelegramMediaImage {
|
||||
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||
if case .Fetching = value.fetchStatus {
|
||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
|
@ -51,13 +51,14 @@ public final class ListMessageItem: ListViewItem {
|
||||
public let selection: ChatHistoryMessageSelection
|
||||
let hintIsLink: Bool
|
||||
let isGlobalSearchResult: Bool
|
||||
let isDownloadList: Bool
|
||||
let displayBackground: Bool
|
||||
|
||||
let header: ListViewItemHeader?
|
||||
|
||||
public let selectable: Bool = true
|
||||
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, displayBackground: Bool = false) {
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayBackground: Bool = false) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
@ -73,6 +74,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
self.selection = selection
|
||||
self.hintIsLink = hintIsLink
|
||||
self.isGlobalSearchResult = isGlobalSearchResult
|
||||
self.isDownloadList = isDownloadList
|
||||
self.displayBackground = displayBackground
|
||||
}
|
||||
|
||||
|
@ -142,6 +142,11 @@ public final class MediaBox {
|
||||
private let cacheQueue = Queue()
|
||||
private let timeBasedCleanup: TimeBasedCleanup
|
||||
|
||||
private let didRemoveResourcesPipe = ValuePipe<Void>()
|
||||
public var didRemoveResources: Signal<Void, NoError> {
|
||||
return .single(Void()) |> then(self.didRemoveResourcesPipe.signal())
|
||||
}
|
||||
|
||||
private var statusContexts: [MediaResourceId: ResourceStatusContext] = [:]
|
||||
private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:]
|
||||
|
||||
@ -310,11 +315,15 @@ public final class MediaBox {
|
||||
}
|
||||
|
||||
public func resourceStatus(_ resource: MediaResource, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> {
|
||||
return self.resourceStatus(resource.id, resourceSize: resource.size, approximateSynchronousValue: approximateSynchronousValue)
|
||||
}
|
||||
|
||||
public func resourceStatus(_ resourceId: MediaResourceId, resourceSize: Int?, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> {
|
||||
let signal = Signal<MediaResourceStatus, NoError> { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.concurrentQueue.async {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
let paths = self.storePathsForId(resourceId)
|
||||
|
||||
if let _ = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
@ -324,7 +333,6 @@ public final class MediaBox {
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
self.statusQueue.async {
|
||||
let resourceId = resource.id
|
||||
let statusContext: ResourceStatusContext
|
||||
var statusUpdateDisposable: MetaDisposable?
|
||||
if let current = self.statusContexts[resourceId] {
|
||||
@ -347,7 +355,7 @@ public final class MediaBox {
|
||||
if let statusUpdateDisposable = statusUpdateDisposable {
|
||||
let statusQueue = self.statusQueue
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource.id) {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resourceId) {
|
||||
let statusDisposable = fileContext.status(next: { [weak statusContext] value in
|
||||
statusQueue.async {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext, current.status != value {
|
||||
@ -367,7 +375,7 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, size: resource.size.flatMap(Int32.init))
|
||||
}, size: resourceSize.flatMap(Int32.init))
|
||||
statusUpdateDisposable.set(ActionDisposable {
|
||||
statusDisposable.dispose()
|
||||
releaseContext()
|
||||
@ -378,10 +386,10 @@ public final class MediaBox {
|
||||
|
||||
disposable.set(ActionDisposable { [weak statusContext] in
|
||||
self.statusQueue.async {
|
||||
if let current = self.statusContexts[resource.id], current === statusContext {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext {
|
||||
current.subscribers.remove(index)
|
||||
if current.subscribers.isEmpty {
|
||||
self.statusContexts.removeValue(forKey: resource.id)
|
||||
self.statusContexts.removeValue(forKey: resourceId)
|
||||
current.disposable.dispose()
|
||||
}
|
||||
}
|
||||
@ -395,10 +403,10 @@ public final class MediaBox {
|
||||
}
|
||||
if approximateSynchronousValue {
|
||||
return Signal<Signal<MediaResourceStatus, NoError>, NoError> { subscriber in
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
let paths = self.storePathsForId(resourceId)
|
||||
if let _ = fileSize(paths.complete) {
|
||||
subscriber.putNext(.single(.Local))
|
||||
} else if let size = fileSize(paths.partial), size == resource.size {
|
||||
} else if let size = fileSize(paths.partial), size == resourceSize {
|
||||
subscriber.putNext(.single(.Local))
|
||||
} else {
|
||||
subscriber.putNext(.single(.Remote) |> then(signal))
|
||||
@ -1287,7 +1295,7 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false) -> Signal<Float, NoError> {
|
||||
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
let uniqueIds = Set(ids.map { $0.stringRepresentation })
|
||||
@ -1352,6 +1360,19 @@ public final class MediaBox {
|
||||
reportProgress(count)
|
||||
}
|
||||
|
||||
if notify {
|
||||
for id in ids {
|
||||
if let context = self.statusContexts[id] {
|
||||
context.status = .Remote
|
||||
for f in context.subscribers.copyItems() {
|
||||
f(.Remote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.didRemoveResourcesPipe.putNext(Void())
|
||||
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
|
@ -16,6 +16,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1053,9 +1053,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
|
||||
self.textBackgroundNode.isHidden = true
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
|
||||
if let node = node, let accessoryComponentView = node.accessoryComponentView {
|
||||
accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
accessoryComponentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
})
|
||||
|
||||
let transitionBackgroundNode = ASDisplayNode()
|
||||
|
@ -4,6 +4,7 @@ import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import ComponentFlow
|
||||
|
||||
private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe")
|
||||
|
||||
@ -35,6 +36,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
|
||||
public private(set) var placeholderString: NSAttributedString?
|
||||
|
||||
var accessoryComponentView: ComponentHostView<Empty>?
|
||||
|
||||
convenience public override init() {
|
||||
self.init(fieldStyle: .legacy)
|
||||
}
|
||||
@ -105,6 +108,29 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
public func setAccessoryComponent(component: AnyComponent<Empty>?) {
|
||||
if let component = component {
|
||||
let accessoryComponentView: ComponentHostView<Empty>
|
||||
if let current = self.accessoryComponentView {
|
||||
accessoryComponentView = current
|
||||
} else {
|
||||
accessoryComponentView = ComponentHostView()
|
||||
self.accessoryComponentView = accessoryComponentView
|
||||
self.view.addSubview(accessoryComponentView)
|
||||
}
|
||||
let accessorySize = accessoryComponentView.update(
|
||||
transition: .immediate,
|
||||
component: component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
accessoryComponentView.frame = CGRect(origin: CGPoint(x: self.bounds.width - accessorySize.width - 4.0, y: floor((self.bounds.height - accessorySize.height) / 2.0)), size: accessorySize)
|
||||
} else if let accessoryComponentView = self.accessoryComponentView {
|
||||
self.accessoryComponentView = nil
|
||||
accessoryComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ expansionProgress: CGFloat, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor, _ transition: ContainedViewLayoutTransition) -> (CGFloat, () -> Void) {
|
||||
let labelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let currentForegroundColor = self.foregroundColor
|
||||
@ -183,6 +209,10 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
|
||||
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
|
||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)))
|
||||
|
||||
if let accessoryComponentView = strongSelf.accessoryComponentView {
|
||||
accessoryComponentView.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentView.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentView.bounds.height) / 2.0)), size: accessoryComponentView.bounds.size)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,337 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ReactionImageComponent
|
||||
import WebPBinding
|
||||
import FetchManagerImpl
|
||||
import ListMessageItem
|
||||
import ListSectionHeaderNode
|
||||
|
||||
private struct DownloadItem: Equatable {
|
||||
let resourceId: MediaResourceId
|
||||
let message: Message
|
||||
let priority: FetchManagerPriorityKey
|
||||
|
||||
static func ==(lhs: DownloadItem, rhs: DownloadItem) -> Bool {
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.priority != rhs.priority {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class DownloadsControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
) {
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
private enum DownloadsControllerSection: Int32 {
|
||||
case items
|
||||
}
|
||||
|
||||
public final class DownloadsItemHeader: ListViewItemHeader {
|
||||
public let id: ListViewItemNode.HeaderId
|
||||
public let title: String
|
||||
public let stickDirection: ListViewItemHeaderStickDirection = .top
|
||||
public let stickOverInsets: Bool = true
|
||||
public let theme: PresentationTheme
|
||||
|
||||
public let height: CGFloat = 28.0
|
||||
|
||||
public init(id: ListViewItemNode.HeaderId, title: String, theme: PresentationTheme) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
public func combinesWith(other: ListViewItemHeader) -> Bool {
|
||||
if let other = other as? DownloadsItemHeader, other.id == self.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
|
||||
return DownloadsItemHeaderNode(title: self.title, theme: self.theme)
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
|
||||
(node as? DownloadsItemHeaderNode)?.update(title: self.title)
|
||||
}
|
||||
}
|
||||
|
||||
public final class DownloadsItemHeaderNode: ListViewItemHeaderNode {
|
||||
private var title: String
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
|
||||
|
||||
private let sectionHeaderNode: ListSectionHeaderNode
|
||||
|
||||
public init(title: String, theme: PresentationTheme) {
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
|
||||
self.sectionHeaderNode = ListSectionHeaderNode(theme: theme)
|
||||
|
||||
super.init()
|
||||
|
||||
self.sectionHeaderNode.title = title
|
||||
self.sectionHeaderNode.action = nil
|
||||
|
||||
self.addSubnode(self.sectionHeaderNode)
|
||||
}
|
||||
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
self.sectionHeaderNode.updateTheme(theme: theme)
|
||||
}
|
||||
|
||||
public func update(title: String) {
|
||||
self.sectionHeaderNode.title = title
|
||||
self.sectionHeaderNode.action = nil
|
||||
|
||||
if let (size, leftInset, rightInset) = self.validLayout {
|
||||
self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.validLayout = (size, leftInset, rightInset)
|
||||
self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
|
||||
override public func animateRemoved(duration: Double) {
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true)
|
||||
}
|
||||
}
|
||||
|
||||
private enum DownloadsControllerEntry: ItemListNodeEntry {
|
||||
enum StableId: Hashable {
|
||||
case item(MediaResourceId)
|
||||
}
|
||||
|
||||
case item(item: DownloadItem)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .item:
|
||||
return DownloadsControllerSection.items.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: StableId {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return .item(item.resourceId)
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: FetchManagerPriorityKey {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return item.priority
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: DownloadsControllerEntry, rhs: DownloadsControllerEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .item(item):
|
||||
if case .item(item) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: DownloadsControllerEntry, rhs: DownloadsControllerEntry) -> Bool {
|
||||
return lhs.sortId < rhs.sortId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! DownloadsControllerArguments
|
||||
let _ = arguments
|
||||
switch self {
|
||||
case let .item(item):
|
||||
let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in
|
||||
return false
|
||||
}, openMessageContextMenu: { message, _, node, rect, gesture in
|
||||
}, toggleMessagesSelection: { messageId, selected in
|
||||
}, openUrl: { url, _, _, message in
|
||||
}, openInstantPage: { message, data in
|
||||
}, longTap: { action, message in
|
||||
}, getHiddenMedia: {
|
||||
return [:]
|
||||
})
|
||||
|
||||
let presentationData = arguments.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: arguments.context, chatLocation: .peer(item.message.id.peerId), interaction: listInteraction, message: item.message, selection: .none, displayHeader: false, customHeader: nil/*DownloadsItemHeader(id: ListViewItemNode.HeaderId(space: 0, id: item.message.id.peerId), title: item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "", theme: presentationData.theme)*/, hintIsLink: false, isGlobalSearchResult: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadsControllerState: Equatable {
|
||||
var hasReaction: Bool = false
|
||||
}
|
||||
|
||||
private func downloadsControllerEntries(
|
||||
presentationData: PresentationData,
|
||||
items: [DownloadItem],
|
||||
state: DownloadsControllerState
|
||||
) -> [DownloadsControllerEntry] {
|
||||
var entries: [DownloadsControllerEntry] = []
|
||||
|
||||
var index = 0
|
||||
for item in items {
|
||||
entries.append(.item(
|
||||
item: item
|
||||
))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func downloadsController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil
|
||||
) -> ViewController {
|
||||
let statePromise = ValuePromise(DownloadsControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: DownloadsControllerState())
|
||||
let updateState: ((DownloadsControllerState) -> DownloadsControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
let _ = updateState
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let _ = dismissImpl
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let arguments = DownloadsControllerArguments(
|
||||
context: context
|
||||
)
|
||||
|
||||
let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
|
||||
|> map { preferencesView -> ReactionSettings in
|
||||
let reactionSettings: ReactionSettings
|
||||
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
|
||||
reactionSettings = value
|
||||
} else {
|
||||
reactionSettings = .default
|
||||
}
|
||||
return reactionSettings
|
||||
}
|
||||
|
||||
let downloadItems: Signal<[DownloadItem], NoError> = (context.fetchManager as! FetchManagerImpl).entriesSummary
|
||||
|> mapToSignal { entries -> Signal<[DownloadItem], NoError> in
|
||||
var itemSignals: [Signal<DownloadItem?, NoError>] = []
|
||||
|
||||
for entry in entries {
|
||||
switch entry.id.locationKey {
|
||||
case let .messageId(id):
|
||||
itemSignals.append(context.account.postbox.transaction { transaction -> DownloadItem? in
|
||||
if let message = transaction.getMessage(id) {
|
||||
return DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), itemSignals)
|
||||
|> map { items -> [DownloadItem] in
|
||||
return items.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
presentationData,
|
||||
statePromise.get(),
|
||||
context.engine.stickers.availableReactions(),
|
||||
settings,
|
||||
downloadItems
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, availableReactions, settings, downloadItems -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
//TODO:localize
|
||||
let title: String = "Downloads"
|
||||
|
||||
let entries = downloadsControllerEntries(
|
||||
presentationData: presentationData,
|
||||
items: downloadItems,
|
||||
state: state
|
||||
)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text(title),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .plain,
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
controller.didScrollWithOffset = { [weak controller] offset, transition, _ in
|
||||
guard let controller = controller else {
|
||||
return
|
||||
}
|
||||
controller.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ReactionChatPreviewItemNode {
|
||||
itemNode.standaloneReactionAnimation?.addRelativeContentOffset(CGPoint(x: 0.0, y: offset), transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismissImpl = { [weak controller] in
|
||||
guard let controller = controller else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ public struct Namespaces {
|
||||
public static let RecentlyUsedHashtags: Int32 = 8
|
||||
public static let CloudThemes: Int32 = 9
|
||||
public static let CloudGreetingStickers: Int32 = 10
|
||||
public static let RecentDownloads: Int32 = 11
|
||||
}
|
||||
|
||||
public struct CachedItemCollection {
|
||||
|
@ -0,0 +1,201 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class RecentDownloadItem: Codable, Equatable {
|
||||
struct Id {
|
||||
var rawValue: MemoryBuffer
|
||||
|
||||
init(id: MessageId, resourceId: String) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var idId: Int32 = id.id
|
||||
buffer.write(&idId, length: 4)
|
||||
|
||||
var idNamespace: Int32 = id.namespace
|
||||
buffer.write(&idNamespace, length: 4)
|
||||
|
||||
var peerId: Int64 = id.peerId.toInt64()
|
||||
buffer.write(&peerId, length: 8)
|
||||
|
||||
let resourceIdData = resourceId.data(using: .utf8)!
|
||||
var resourceIdLength = Int32(resourceIdData.count)
|
||||
buffer.write(&resourceIdLength, length: 4)
|
||||
buffer.write(resourceIdData)
|
||||
|
||||
self.rawValue = buffer.makeReadBufferAndReset()
|
||||
}
|
||||
}
|
||||
|
||||
public let messageId: MessageId
|
||||
public let resourceId: String
|
||||
public let timestamp: Int32
|
||||
public let isSeen: Bool
|
||||
|
||||
public init(messageId: MessageId, resourceId: String, timestamp: Int32, isSeen: Bool) {
|
||||
self.messageId = messageId
|
||||
self.resourceId = resourceId
|
||||
self.timestamp = timestamp
|
||||
self.isSeen = isSeen
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.messageId = try container.decode(MessageId.self, forKey: "messageId")
|
||||
self.resourceId = try container.decode(String.self, forKey: "resourceId")
|
||||
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
|
||||
self.isSeen = try container.decodeIfPresent(Bool.self, forKey: "isSeen") ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.messageId, forKey: "messageId")
|
||||
try container.encode(self.resourceId, forKey: "resourceId")
|
||||
try container.encode(self.timestamp, forKey: "timestamp")
|
||||
try container.encode(self.isSeen, forKey: "isSeen")
|
||||
}
|
||||
|
||||
public static func ==(lhs: RecentDownloadItem, rhs: RecentDownloadItem) -> Bool {
|
||||
if lhs.messageId != rhs.messageId {
|
||||
return false
|
||||
}
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeen != rhs.isSeen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withSeen() -> RecentDownloadItem {
|
||||
return RecentDownloadItem(messageId: self.messageId, resourceId: self.resourceId, timestamp: self.timestamp, isSeen: true)
|
||||
}
|
||||
}
|
||||
|
||||
public final class RenderedRecentDownloadItem: Equatable {
|
||||
public let message: Message
|
||||
public let timestamp: Int32
|
||||
public let isSeen: Bool
|
||||
public let resourceId: String
|
||||
|
||||
public init(message: Message, timestamp: Int32, isSeen: Bool, resourceId: String) {
|
||||
self.message = message
|
||||
self.timestamp = timestamp
|
||||
self.isSeen = isSeen
|
||||
self.resourceId = resourceId
|
||||
}
|
||||
|
||||
public static func ==(lhs: RenderedRecentDownloadItem, rhs: RenderedRecentDownloadItem) -> Bool {
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.message.stableVersion != rhs.message.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeen != rhs.isSeen {
|
||||
return false
|
||||
}
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownloadItem], NoError> {
|
||||
let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.RecentDownloads)
|
||||
return postbox.combinedView(keys: [viewKey])
|
||||
|> mapToSignal { views -> Signal<[RenderedRecentDownloadItem], NoError> in
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
return combineLatest(postbox.transaction { transaction -> [RenderedRecentDownloadItem] in
|
||||
var result: [RenderedRecentDownloadItem] = []
|
||||
|
||||
for item in view.items {
|
||||
guard let item = item.contents.get(RecentDownloadItem.self) else {
|
||||
continue
|
||||
}
|
||||
guard let message = transaction.getMessage(item.messageId) else {
|
||||
continue
|
||||
}
|
||||
result.append(RenderedRecentDownloadItem(message: message, timestamp: item.timestamp, isSeen: item.isSeen, resourceId: item.resourceId))
|
||||
}
|
||||
|
||||
return result
|
||||
}, postbox.mediaBox.didRemoveResources)
|
||||
|> mapToSignal { items, _ -> Signal<[RenderedRecentDownloadItem], NoError> in
|
||||
var statusSignals: [Signal<Bool, NoError>] = []
|
||||
|
||||
for item in items {
|
||||
statusSignals.append(postbox.mediaBox.resourceStatus(MediaResourceId(item.resourceId), resourceSize: nil)
|
||||
|> map { status -> Bool in
|
||||
switch status {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged)
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), statusSignals)
|
||||
|> map { statuses -> [RenderedRecentDownloadItem] in
|
||||
var result: [RenderedRecentDownloadItem] = []
|
||||
for i in 0 ..< items.count {
|
||||
if statuses[i] {
|
||||
result.append(items[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func addRecentDownloadItem(postbox: Postbox, item: RecentDownloadItem) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
guard let entry = CodableEntry(item) else {
|
||||
return
|
||||
}
|
||||
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentDownloads, item: OrderedItemListEntry(id: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue, contents: entry), removeTailIfCountExceeds: 200)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func markAllRecentDownloadItemsAsSeen(postbox: Postbox) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads)
|
||||
var hasUnseen = false
|
||||
for item in items {
|
||||
if let item = item.contents.get(RecentDownloadItem.self), !item.isSeen {
|
||||
hasUnseen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUnseen {
|
||||
return
|
||||
}
|
||||
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads, items: items.compactMap { item -> OrderedItemListEntry? in
|
||||
guard let item = item.contents.get(RecentDownloadItem.self) else {
|
||||
return nil
|
||||
}
|
||||
guard let entry = CodableEntry(item.withSeen()) else {
|
||||
return nil
|
||||
}
|
||||
return OrderedItemListEntry(id: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue, contents: entry)
|
||||
})
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -403,7 +403,6 @@ private enum PeerInfoSettingsSection {
|
||||
case avatar
|
||||
case edit
|
||||
case proxy
|
||||
case downloads
|
||||
case savedMessages
|
||||
case recentCalls
|
||||
case devices
|
||||
@ -663,10 +662,6 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "Downloads", icon: PresentationResourcesSettings.savedMessages, action: {
|
||||
interaction.openSettings(.downloads)
|
||||
}))
|
||||
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_SavedMessages, icon: PresentationResourcesSettings.savedMessages, action: {
|
||||
interaction.openSettings(.savedMessages)
|
||||
}))
|
||||
@ -5532,8 +5527,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
|
||||
case .proxy:
|
||||
self.controller?.push(proxySettingsController(context: self.context))
|
||||
case .downloads:
|
||||
self.controller?.push(downloadsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData))
|
||||
case .savedMessages:
|
||||
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.context.account.peerId)))
|
||||
|
@ -1128,6 +1128,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
navigateToChatControllerImpl(params)
|
||||
}
|
||||
|
||||
public func openStorageUsage(context: AccountContext) {
|
||||
guard let navigationController = self.mainWindow?.viewController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = storageUsageController(context: context, isModal: true)
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
|
||||
public func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) {
|
||||
var found = false
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user