Download list improvements

This commit is contained in:
Ali 2022-02-15 23:40:44 +04:00
parent 307f8a841d
commit 20748909d1
27 changed files with 1102 additions and 452 deletions

View File

@ -501,6 +501,7 @@ public final class ContactSelectionControllerParams {
public enum ChatListSearchFilter: Equatable { public enum ChatListSearchFilter: Equatable {
case chats case chats
case media case media
case downloads
case links case links
case files case files
case music case music
@ -514,14 +515,16 @@ public enum ChatListSearchFilter: Equatable {
return 0 return 0
case .media: case .media:
return 1 return 1
case .links: case .downloads:
return 2 return 2
case .files: case .links:
return 3 return 3
case .music: case .files:
return 4 return 4
case .voice: case .music:
return 5 return 5
case .voice:
return 6
case let .peer(peerId, _, _, _): case let .peer(peerId, _, _, _):
return peerId.id._internalGetInt64Value() return peerId.id._internalGetInt64Value()
case let .date(_, date, _): case let .date(_, date, _):
@ -616,6 +619,7 @@ public protocol SharedAccountContext: AnyObject {
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToChatController(_ params: NavigateToChatControllerParams)
func openStorageUsage(context: AccountContext)
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) 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 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> func chatAvailableMessageActions(postbox: Postbox, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>

View File

@ -27,6 +27,8 @@ public enum ChatListSearchItemHeaderType {
case recentCalls case recentCalls
case orImportIntoAnExistingGroup case orImportIntoAnExistingGroup
case subscribers case subscribers
case downloading
case recentDownloads
fileprivate func title(strings: PresentationStrings) -> String { fileprivate func title(strings: PresentationStrings) -> String {
switch self { switch self {
@ -74,6 +76,12 @@ public enum ChatListSearchItemHeaderType {
return strings.ChatList_HeaderImportIntoAnExistingGroup return strings.ChatList_HeaderImportIntoAnExistingGroup
case .subscribers: case .subscribers:
return strings.Channel_ChannelSubscribersHeader 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 return .orImportIntoAnExistingGroup
case .subscribers: case .subscribers:
return .subscribers return .subscribers
case .downloading:
return .downloading
case .recentDownloads:
return .recentDownloads
} }
} }
} }
@ -154,6 +166,8 @@ private enum ChatListSearchItemHeaderId: Int32 {
case recentCalls case recentCalls
case orImportIntoAnExistingGroup case orImportIntoAnExistingGroup
case subscribers case subscribers
case downloading
case recentDownloads
} }
public final class ChatListSearchItemHeader: ListViewItemHeader { public final class ChatListSearchItemHeader: ListViewItemHeader {

View File

@ -110,7 +110,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
let searchBarNodeLayout = self.searchBarNode.asyncLayout() let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder 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 baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor 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()) 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 { if let strongSelf = self {
let transition: ContainedViewLayoutTransition let transition: ContainedViewLayoutTransition
if animated { if animated {

View File

@ -62,6 +62,9 @@ swift_library(
"//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/StickerResources:StickerResources", "//submodules/StickerResources:StickerResources",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/FetchManagerImpl:FetchManagerImpl",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -25,6 +25,9 @@ import TooltipUI
import TelegramCallsUI import TelegramCallsUI
import StickerResources import StickerResources
import PasswordSetupUI import PasswordSetupUI
import FetchManagerImpl
import ComponentFlow
import LottieAnimationComponent
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging { if listNode.scroller.isDragging {
@ -151,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let tabContainerNode: ChatListFilterTabContainerNode private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], Bool)? private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
private var activeDownloadsDisposable: Disposable?
private var didSetupTabs = false private var didSetupTabs = false
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { 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.searchContentNode?.updateExpansionProgress(0.0)
self.navigationBar?.setContentNode(self.searchContentNode, animated: false) 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 { if enableDebugActions {
@ -514,6 +608,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.stateDisposable.dispose() self.stateDisposable.dispose()
self.filterDisposable.dispose() self.filterDisposable.dispose()
self.featuredFiltersDisposable.dispose() self.featuredFiltersDisposable.dispose()
self.activeDownloadsDisposable?.dispose()
} }
private func updateThemeAndStrings() { private func updateThemeAndStrings() {

View File

@ -29,6 +29,7 @@ import ChatInterfaceState
import ShareController import ShareController
import UndoUI import UndoUI
import TextFormat import TextFormat
import Postbox
private enum ChatListTokenId: Int32 { private enum ChatListTokenId: Int32 {
case archive case archive
@ -45,14 +46,14 @@ final class ChatListSearchInteraction {
let clearRecentSearch: () -> Void let clearRecentSearch: () -> Void
let addContact: (String) -> Void let addContact: (String) -> Void
let toggleMessageSelection: (EngineMessage.Id, Bool) -> 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 mediaMessageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
let peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)? let peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
let present: (ViewController, Any?) -> Void let present: (ViewController, Any?) -> Void
let dismissInput: () -> Void let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>? 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.openPeer = openPeer
self.openDisabledPeer = openDisabledPeer self.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage self.openMessage = openMessage
@ -112,8 +113,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var stateValue = ChatListSearchContainerNodeSearchState() private var stateValue = ChatListSearchContainerNodeSearchState()
private let statePromise = ValuePromise<ChatListSearchContainerNodeSearchState>() private let statePromise = ValuePromise<ChatListSearchContainerNodeSearchState>()
private var selectedFilterKey: ChatListSearchFilterEntryId? private var selectedFilter: ChatListSearchFilterEntry?
private var selectedFilterKeyPromise = Promise<ChatListSearchFilterEntryId?>() private var selectedFilterPromise = Promise<ChatListSearchFilterEntry?>()
private var transitionFraction: CGFloat = 0.0 private var transitionFraction: CGFloat = 0.0
private weak var copyProtectionTooltipController: TooltipController? private weak var copyProtectionTooltipController: TooltipController?
@ -134,8 +135,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.navigationController = navigationController self.navigationController = navigationController
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.selectedFilterKey = .filter(initialFilter.id) self.selectedFilter = .filter(initialFilter)
self.selectedFilterKeyPromise.set(.single(self.selectedFilterKey)) self.selectedFilterPromise.set(.single(self.selectedFilter))
self.openMessage = originalOpenMessage self.openMessage = originalOpenMessage
self.present = present self.present = present
@ -217,8 +218,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
return state.withUpdatedSelectedMessageIds(selectedMessageIds) return state.withUpdatedSelectedMessageIds(selectedMessageIds)
} }
} }
}, messageContextAction: { [weak self] message, node, rect, gesture in }, messageContextAction: { [weak self] message, node, rect, gesture, paneKey, downloadResourceId in
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture) self?.messageContextAction(message, node: node, rect: rect, gesture: gesture, paneKey: paneKey, downloadResourceId: downloadResourceId)
}, mediaMessageContextAction: { [weak self] message, node, rect, gesture in }, mediaMessageContextAction: { [weak self] message, node, rect, gesture in
self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture) self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture)
}, peerContextAction: { peer, source, node, gesture in }, peerContextAction: { peer, source, node, gesture in
@ -244,6 +245,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
filterKey = .chats filterKey = .chats
case .media: case .media:
filterKey = .media filterKey = .media
case .downloads:
filterKey = .downloads
case .links: case .links:
filterKey = .links filterKey = .links
case .files: case .files:
@ -253,8 +256,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
case .voice: case .voice:
filterKey = .voice filterKey = .voice
} }
strongSelf.selectedFilterKey = .filter(filterKey.id) strongSelf.selectedFilter = .filter(filterKey)
strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey)) strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter))
strongSelf.transitionFraction = transitionFraction strongSelf.transitionFraction = transitionFraction
if let (layout, _) = strongSelf.validLayout { if let (layout, _) = strongSelf.validLayout {
@ -262,9 +265,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters filters = suggestedFilters
} else { } 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 key = .chats
case .media: case .media:
key = .media key = .media
case .downloads:
key = .downloads
case .links: case .links:
key = .links key = .links
case .files: case .files:
@ -306,7 +311,31 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.filterContainerNode.filterPressed?(initialFilter) 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 |> mapToSignal { query -> Signal<[EnginePeer], NoError> in
if let query = query { if let query = query {
return context.account.postbox.searchPeers(query: query.lowercased()) 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) let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> take(1) |> 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 |> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in
if searchQuery?.isEmpty ?? true { if searchQuery?.isEmpty ?? true {
return .single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer))) return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))
} else { } else {
return (.complete() |> delay(0.25, queue: Queue.mainQueue())) 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 } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in
var suggestedFilters: [ChatListSearchFilter] = [] var suggestedFilters: [ChatListSearchFilter] = []
@ -523,6 +552,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music key = .music
case .voice: case .voice:
key = .voice key = .voice
case .downloads:
key = .downloads
default: default:
key = .chats key = .chats
} }
@ -553,7 +584,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
let overflowInset: CGFloat = 20.0 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 var bottomIntrinsicInset = layout.intrinsicInsets.bottom
if case .root = self.groupId { if case .root = self.groupId {
@ -737,10 +768,137 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let _ = self.paneContainerNode.scrollToTop() 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 { guard let node = node as? ContextExtractedContentContainingNode else {
return 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() let _ = storedMessageFromSearch(account: self.context.account, message: message._asMessage()).start()
var linkForCopying: String? var linkForCopying: String?
@ -756,8 +914,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
} }
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
let context = self.context let context = self.context
let (peers, messages) = self.currentMessages 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) 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 ignoreContentTouches: Bool = true
let blurBackground: Bool = true let blurBackground: Bool = true
let shouldBeDismissed: Signal<Bool, NoError>
private let sourceNode: ContextExtractedContentContainingNode private let sourceNode: ContextExtractedContentContainingNode
init(sourceNode: ContextExtractedContentContainingNode) { init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal<Bool, NoError>? = nil) {
self.sourceNode = sourceNode self.sourceNode = sourceNode
self.shouldBeDismissed = shouldBeDismissed ?? .single(false)
} }
func takeView() -> ContextControllerTakeViewInfo? { func takeView() -> ContextControllerTakeViewInfo? {

View File

@ -84,6 +84,10 @@ private final class ItemNode: ASDisplayNode {
case .media: case .media:
title = presentationData.strings.ChatList_Search_FilterMedia title = presentationData.strings.ChatList_Search_FilterMedia
icon = nil icon = nil
case .downloads:
//TODO:localize
title = "Downloads"
icon = nil
case .links: case .links:
title = presentationData.strings.ChatList_Search_FilterLinks title = presentationData.strings.ChatList_Search_FilterLinks
icon = nil icon = nil

View File

@ -26,6 +26,8 @@ import AppBundle
import ShimmerEffect import ShimmerEffect
import ChatListSearchRecentPeersNode import ChatListSearchRecentPeersNode
import UndoUI import UndoUI
import Postbox
import FetchManagerImpl
private enum ChatListRecentEntryStableId: Hashable { private enum ChatListRecentEntryStableId: Hashable {
case topPeers case topPeers
@ -217,7 +219,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
public enum ChatListSearchEntryStableId: Hashable { public enum ChatListSearchEntryStableId: Hashable {
case localPeerId(EnginePeer.Id) case localPeerId(EnginePeer.Id)
case globalPeerId(EnginePeer.Id) case globalPeerId(EnginePeer.Id)
case messageId(EngineMessage.Id) case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
case addContact case addContact
} }
@ -228,9 +230,54 @@ public enum ChatListSearchSectionExpandType {
} }
public enum ChatListSearchEntry: Comparable, Identifiable { 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 localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
case globalPeer(FoundPeer, (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) case addContact(String, PresentationTheme, PresentationStrings)
public var stableId: ChatListSearchEntryStableId { public var stableId: ChatListSearchEntryStableId {
@ -239,8 +286,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id) return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _): case let .globalPeer(peer, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id) return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _): case let .message(message, _, _, _, _, _, _, _, _, section):
return .messageId(message.id) return .messageId(message.id, section)
case .addContact: case .addContact:
return .addContact return .addContact
} }
@ -260,8 +307,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader): case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection):
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = rhs { if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection) = rhs {
if lhsMessage.id != rhsMessage.id { if lhsMessage.id != rhsMessage.id {
return false return false
} }
@ -286,6 +333,15 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
if lhsDisplayCustomHeader != rhsDisplayCustomHeader { if lhsDisplayCustomHeader != rhsDisplayCustomHeader {
return false return false
} }
if lhsKey != rhsKey {
return false
}
if lhsResourceId != rhsResourceId {
return false
}
if lhsSection != rhsSection {
return false
}
return true return true
} else { } else {
return false return false
@ -325,9 +381,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .message, .addContact: case .message, .addContact:
return true return true
} }
case let .message(lhsMessage, _, _, _, _, _, _): case let .message(_, _, _, _, _, _, _, lhsKey, _, _):
if case let .message(rhsMessage, _, _, _, _, _, _) = rhs { if case let .message(_, _, _, _, _, _, _, rhsKey, _, _) = rhs {
return lhsMessage.index < rhsMessage.index return lhsKey < rhsKey
} else if case .addContact = rhs { } else if case .addContact = rhs {
return true return true
} else { } 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 { switch self {
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType):
let primaryPeer: EnginePeer let primaryPeer: EnginePeer
@ -486,11 +542,29 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture) peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture)
} }
}) })
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader): case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _):
let header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) 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 let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
var isMedia = false
if let tagMask = tagMask, tagMask != .photoOrVideo { 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 { } 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) 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) 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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, 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, 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) 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 { final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let context: AccountContext private let context: AccountContext
private let interaction: ChatListSearchInteraction private let interaction: ChatListSearchInteraction
@ -704,6 +797,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
tagMask = nil tagMask = nil
case .media: case .media:
tagMask = .photoOrVideo tagMask = .photoOrVideo
case .downloads:
tagMask = nil
case .links: case .links:
tagMask = .webPage tagMask = .webPage
case .files: case .files:
@ -811,13 +906,120 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let presentationDataPromise = self.presentationDataPromise let presentationDataPromise = self.presentationDataPromise
let searchStatePromise = self.searchStatePromise let searchStatePromise = self.searchStatePromise
let selectionPromise = self.selectedMessagesPromise let selectionPromise = self.selectedMessagesPromise
let foundItems = combineLatest(searchQuery, searchOptions)
|> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in let downloadItems: Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError>
if query == nil && options == nil && tagMask == nil { 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) let _ = currentRemotePeers.swap(nil)
return .single(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 accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1)
let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError> let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError>
if let query = query { if let query = query {
@ -1108,7 +1310,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) 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 index += 1
} }
@ -1131,7 +1333,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) 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 index += 1
} }
} }
@ -1249,8 +1451,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}) })
let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in
guard let strongSelf = self else {
return false
}
interaction.dismissInput() 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() interaction.dismissInput()
}, present: { c, a in }, present: { c, a in
interaction.present(c, a) interaction.present(c, a)
@ -1277,13 +1495,25 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return (message.map { $0._asMessage() }, a, b) return (message.map { $0._asMessage() }, a, b)
}, at: message.id, loadMore: { }, at: message.id, loadMore: {
loadMore() loadMore()
}), gallerySource: .custom(messages: foundMessages |> map { message, a, b in }), gallerySource: gallerySource))
return (message.map { $0._asMessage() }, a, b) }, openMessageContextMenu: { [weak self] message, _, node, rect, gesture in
}, messageId: message.id, loadMore: { guard let strongSelf = self, let currentEntries = strongSelf.currentEntries else {
loadMore() return
}))) }
}, openMessageContextMenu: { message, _, node, rect, gesture in
interaction.messageContextAction(EngineMessage(message), node, rect, gesture) 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 }, toggleMessagesSelection: { messageId, selected in
if let messageId = messageId.first { if let messageId = messageId.first {
interaction.toggleMessageSelection(messageId, selected) interaction.toggleMessageSelection(messageId, selected)
@ -1355,7 +1585,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
let firstTime = previousEntries == 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) interaction.peerContextAction?(message, node, rect, gesture)
}, toggleExpandLocalResults: { [weak self] in }, toggleExpandLocalResults: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1376,15 +1606,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return state return state
} }
}, searchPeer: { peer in }, searchPeer: { peer in
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResourceId in
interaction.messageContextAction(message, node, rect, gesture) 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.currentEntries = newEntries
strongSelf.enqueueTransition(transition, firstTime: firstTime) strongSelf.enqueueTransition(transition, firstTime: firstTime)
var messages: [EngineMessage] = [] var messages: [EngineMessage] = []
for entry in newEntries { for entry in newEntries {
if case let .message(message, _, _, _, _, _, _) = entry { if case let .message(message, _, _, _, _, _, _, _, _, _) = entry {
messages.append(message) 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 { func scrollToTop() -> Bool {
if !self.mediaNode.isHidden { if !self.mediaNode.isHidden {
return self.mediaNode.scrollToTop() 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.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
} }
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults if strongSelf.key == .downloads {
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.isHidden = true
strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsTitleNode.isHidden = true
strongSelf.emptyResultsAnimationNode.visibility = emptyResults 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 let targetAlpha: CGFloat = displayPlaceholder ? 1.0 : 0.0
if strongSelf.shimmerNode.alpha != targetAlpha { 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) 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 let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
switch key { switch key {
case .chats: case .chats, .downloads:
let message = EngineMessage( let message = EngineMessage(
stableId: 0, stableId: 0,
stableVersion: 0, stableVersion: 0,
@ -2358,7 +2624,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
associatedMessageIds: [] associatedMessageIds: []
) )
let readState = EnginePeerReadCounters() 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: case .media:
return nil return nil
case .links: case .links:

View File

@ -702,7 +702,7 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate {
var index: UInt32 = 0 var index: UInt32 = 0
if let entries = entries { if let entries = entries {
for entry in 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)) self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil))
} }
index += 1 index += 1

View File

@ -19,6 +19,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode {
func updateHiddenMedia() func updateHiddenMedia()
func updateSelectedMessages(animated: Bool) func updateSelectedMessages(animated: Bool)
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
func didBecomeFocused()
var searchCurrentMessages: [EngineMessage]? { get } var searchCurrentMessages: [EngineMessage]? { get }
} }
@ -44,16 +45,17 @@ final class ChatListSearchPaneWrapper {
} }
} }
enum ChatListSearchPaneKey { public enum ChatListSearchPaneKey {
case chats case chats
case media case media
case downloads
case links case links
case files case files
case music case music
case voice case voice
} }
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice] let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice]
struct ChatListSearchPaneSpecifier: Equatable { struct ChatListSearchPaneSpecifier: Equatable {
var key: ChatListSearchPaneKey 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) 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()
}
} }
} }

View 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",
],
)

View File

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

View File

@ -44,6 +44,7 @@ private final class FetchManagerLocationEntry {
let ranges = Bag<IndexSet>() let ranges = Bag<IndexSet>()
var elevatedPriorityReferenceCount: Int32 = 0 var elevatedPriorityReferenceCount: Int32 = 0
var userInitiatedPriorityIndices: [Int32] = [] var userInitiatedPriorityIndices: [Int32] = []
var isPaused: Bool = false
var combinedRanges: IndexSet { var combinedRanges: IndexSet {
var result = IndexSet() var result = IndexSet()
@ -116,7 +117,7 @@ private final class FetchManagerCategoryContext {
private let postbox: Postbox private let postbox: Postbox
private let storeManager: DownloadedMediaStoreManager? private let storeManager: DownloadedMediaStoreManager?
private let entryCompleted: (FetchManagerLocationEntryId) -> Void private let entryCompleted: (FetchManagerLocationEntryId) -> Void
private let activeEntriesUpdated: ([FetchManagerEntrySummary]) -> Void private let activeEntriesUpdated: () -> Void
private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)? private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)?
private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:] private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:]
@ -133,13 +134,17 @@ private final class FetchManagerCategoryContext {
return false 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.postbox = postbox
self.storeManager = storeManager self.storeManager = storeManager
self.entryCompleted = entryCompleted self.entryCompleted = entryCompleted
self.activeEntriesUpdated = activeEntriesUpdated self.activeEntriesUpdated = activeEntriesUpdated
} }
func getActiveEntries() -> [FetchManagerLocationEntry] {
return Array(self.entries.values)
}
func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (AnyMediaReference?, MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) { func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (AnyMediaReference?, MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) {
let entry: FetchManagerLocationEntry let entry: FetchManagerLocationEntry
let previousPriorityKey: FetchManagerPriorityKey? let previousPriorityKey: FetchManagerPriorityKey?
@ -221,8 +226,13 @@ private final class FetchManagerCategoryContext {
parsedRanges = resultRanges parsedRanges = resultRanges
} }
activeContext.disposable?.dispose() 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 |> 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 { if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType) return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> castError(FetchResourceError.self) |> 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 { func maybeFindAndActivateNewTopEntry() -> Bool {
@ -366,8 +376,13 @@ private final class FetchManagerCategoryContext {
}) })
} else if ranges.isEmpty { } else if ranges.isEmpty {
} else { } 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 |> 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 { if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType) return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> castError(FetchResourceError.self) |> castError(FetchResourceError.self)
@ -446,7 +461,7 @@ private final class FetchManagerCategoryContext {
activeContextsUpdated = true 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) { 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 final class FetchManagerImpl: FetchManager {
public let queue = Queue.mainQueue() public let queue = Queue.mainQueue()
private let postbox: Postbox private let postbox: Postbox
@ -545,21 +591,26 @@ public final class FetchManagerImpl: FetchManager {
context.cancelEntry(id, isCompleted: true) context.cancelEntry(id, isCompleted: true)
}) })
} }
}, activeEntriesUpdated: { [weak self] entries in }, activeEntriesUpdated: { [weak self] in
queue.async { queue.async {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
var hasActiveUserInitiatedEntries = false var hasActiveUserInitiatedEntries = false
var activeEntries: [FetchManagerEntrySummary] = []
for (_, context) in strongSelf.categoryContexts { for (_, context) in strongSelf.categoryContexts {
if context.hasActiveUserInitiatedEntries { if context.hasActiveUserInitiatedEntries {
hasActiveUserInitiatedEntries = true 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.hasUserInitiatedEntriesValue.set(hasActiveUserInitiatedEntries)
strongSelf.entriesSummaryValue.set(entries) strongSelf.entriesSummaryValue.set(activeEntries.sorted(by: { $0.priority < $1.priority }))
} }
}) })
self.categoryContexts[key] = context self.categoryContexts[key] = context

View File

@ -45,7 +45,7 @@ public final class HashtagSearchController: TelegramBaseController {
|> map { result, presentationData in |> map { result, presentationData in
let result = result.0 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) 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: { let interaction = ChatListNodeInteraction(activateSearch: {
}, peerSelected: { _, _, _ in }, peerSelected: { _, _, _ in
@ -95,10 +95,10 @@ public final class HashtagSearchController: TelegramBaseController {
}) })
let firstTime = previousEntries == nil 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: { }, toggleExpandGlobalResults: {
}, searchPeer: { _ in }, searchPeer: { _ in
}, searchQuery: "", searchOptions: nil, messageContextAction: nil) }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {})
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
} }
}) })

View File

@ -419,7 +419,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
var descriptionString: String var descriptionString: String
if let performer = performer { if let performer = performer {
if item.isGlobalSearchResult { if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = performer descriptionString = performer
} else { } else {
descriptionString = "\(stringForDuration(Int32(duration)))\(performer)" descriptionString = "\(stringForDuration(Int32(duration)))\(performer)"
@ -430,7 +430,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
descriptionString = "" 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) let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty { if descriptionString.isEmpty {
descriptionString = authorString descriptionString = authorString
@ -474,7 +474,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
authorName = " " 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) 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) let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = "" var descriptionString: String = ""
if let duration = file.duration { if let duration = file.duration {
if item.isGlobalSearchResult { if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = stringForDuration(Int32(duration)) descriptionString = stringForDuration(Int32(duration))
} else { } else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)" descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
} }
} else { } else {
if !item.isGlobalSearchResult { if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = dateString descriptionString = dateString
} }
} }
@ -496,7 +496,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file) iconImage = .roundVideo(file)
} else if !isAudio { } 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) titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var fileExtension: String? var fileExtension: String?
@ -516,18 +516,18 @@ public final class ListMessageFileItemNode: ListMessageNode {
var descriptionString: String = "" var descriptionString: String = ""
if let size = file.size { if let size = file.size {
if item.isGlobalSearchResult { if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)) descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else { } else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)" descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
} }
} else { } else {
if !item.isGlobalSearchResult { if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)" 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) let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty { if descriptionString.isEmpty {
descriptionString = authorString 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) let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = "" var descriptionString: String = ""
if !item.isGlobalSearchResult { if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)" 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) let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty { if descriptionString.isEmpty {
descriptionString = authorString descriptionString = authorString
@ -616,7 +616,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
if statusUpdated { if statusUpdated {
if let file = selectedMedia as? TelegramMediaFile { 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 |> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus { if case .Fetching = value.fetchStatus {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -639,10 +639,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
} }
} }
if isVoice { 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 { } 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 |> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus { if case .Fetching = value.fetchStatus {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())

View File

@ -51,13 +51,14 @@ public final class ListMessageItem: ListViewItem {
public let selection: ChatHistoryMessageSelection public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool let hintIsLink: Bool
let isGlobalSearchResult: Bool let isGlobalSearchResult: Bool
let isDownloadList: Bool
let displayBackground: Bool let displayBackground: Bool
let header: ListViewItemHeader? let header: ListViewItemHeader?
public let selectable: Bool = true 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.presentationData = presentationData
self.context = context self.context = context
self.chatLocation = chatLocation self.chatLocation = chatLocation
@ -73,6 +74,7 @@ public final class ListMessageItem: ListViewItem {
self.selection = selection self.selection = selection
self.hintIsLink = hintIsLink self.hintIsLink = hintIsLink
self.isGlobalSearchResult = isGlobalSearchResult self.isGlobalSearchResult = isGlobalSearchResult
self.isDownloadList = isDownloadList
self.displayBackground = displayBackground self.displayBackground = displayBackground
} }

View File

@ -142,6 +142,11 @@ public final class MediaBox {
private let cacheQueue = Queue() private let cacheQueue = Queue()
private let timeBasedCleanup: TimeBasedCleanup 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 statusContexts: [MediaResourceId: ResourceStatusContext] = [:]
private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:] private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:]
@ -310,11 +315,15 @@ public final class MediaBox {
} }
public func resourceStatus(_ resource: MediaResource, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> { 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 signal = Signal<MediaResourceStatus, NoError> { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.concurrentQueue.async { self.concurrentQueue.async {
let paths = self.storePathsForId(resource.id) let paths = self.storePathsForId(resourceId)
if let _ = fileSize(paths.complete) { if let _ = fileSize(paths.complete) {
self.timeBasedCleanup.touch(paths: [ self.timeBasedCleanup.touch(paths: [
@ -324,7 +333,6 @@ public final class MediaBox {
subscriber.putCompletion() subscriber.putCompletion()
} else { } else {
self.statusQueue.async { self.statusQueue.async {
let resourceId = resource.id
let statusContext: ResourceStatusContext let statusContext: ResourceStatusContext
var statusUpdateDisposable: MetaDisposable? var statusUpdateDisposable: MetaDisposable?
if let current = self.statusContexts[resourceId] { if let current = self.statusContexts[resourceId] {
@ -347,7 +355,7 @@ public final class MediaBox {
if let statusUpdateDisposable = statusUpdateDisposable { if let statusUpdateDisposable = statusUpdateDisposable {
let statusQueue = self.statusQueue let statusQueue = self.statusQueue
self.dataQueue.async { 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 let statusDisposable = fileContext.status(next: { [weak statusContext] value in
statusQueue.async { statusQueue.async {
if let current = self.statusContexts[resourceId], current === statusContext, current.status != value { 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 { statusUpdateDisposable.set(ActionDisposable {
statusDisposable.dispose() statusDisposable.dispose()
releaseContext() releaseContext()
@ -378,10 +386,10 @@ public final class MediaBox {
disposable.set(ActionDisposable { [weak statusContext] in disposable.set(ActionDisposable { [weak statusContext] in
self.statusQueue.async { self.statusQueue.async {
if let current = self.statusContexts[resource.id], current === statusContext { if let current = self.statusContexts[resourceId], current === statusContext {
current.subscribers.remove(index) current.subscribers.remove(index)
if current.subscribers.isEmpty { if current.subscribers.isEmpty {
self.statusContexts.removeValue(forKey: resource.id) self.statusContexts.removeValue(forKey: resourceId)
current.disposable.dispose() current.disposable.dispose()
} }
} }
@ -395,10 +403,10 @@ public final class MediaBox {
} }
if approximateSynchronousValue { if approximateSynchronousValue {
return Signal<Signal<MediaResourceStatus, NoError>, NoError> { subscriber in return Signal<Signal<MediaResourceStatus, NoError>, NoError> { subscriber in
let paths = self.storePathsForId(resource.id) let paths = self.storePathsForId(resourceId)
if let _ = fileSize(paths.complete) { if let _ = fileSize(paths.complete) {
subscriber.putNext(.single(.Local)) 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)) subscriber.putNext(.single(.Local))
} else { } else {
subscriber.putNext(.single(.Remote) |> then(signal)) 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 return Signal { subscriber in
self.dataQueue.async { self.dataQueue.async {
let uniqueIds = Set(ids.map { $0.stringRepresentation }) let uniqueIds = Set(ids.map { $0.stringRepresentation })
@ -1352,6 +1360,19 @@ public final class MediaBox {
reportProgress(count) 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() subscriber.putCompletion()
} }
return EmptyDisposable return EmptyDisposable

View File

@ -16,6 +16,7 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ActivityIndicator:ActivityIndicator", "//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/ComponentFlow:ComponentFlow",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1053,9 +1053,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
self.textBackgroundNode.isHidden = true 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 textBackgroundCompleted = true
intermediateCompletion() 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() let transitionBackgroundNode = ASDisplayNode()

View File

@ -4,6 +4,7 @@ import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import AppBundle import AppBundle
import ComponentFlow
private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe") private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe")
@ -35,6 +36,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
public private(set) var placeholderString: NSAttributedString? public private(set) var placeholderString: NSAttributedString?
var accessoryComponentView: ComponentHostView<Empty>?
convenience public override init() { convenience public override init() {
self.init(fieldStyle: .legacy) 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) { 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 labelLayout = TextNode.asyncLayout(self.labelNode)
let currentForegroundColor = self.foregroundColor let currentForegroundColor = self.foregroundColor
@ -183,6 +209,10 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius) transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha) transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height))) 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)
}
} }
}) })
} }

View File

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

View File

@ -60,6 +60,7 @@ public struct Namespaces {
public static let RecentlyUsedHashtags: Int32 = 8 public static let RecentlyUsedHashtags: Int32 = 8
public static let CloudThemes: Int32 = 9 public static let CloudThemes: Int32 = 9
public static let CloudGreetingStickers: Int32 = 10 public static let CloudGreetingStickers: Int32 = 10
public static let RecentDownloads: Int32 = 11
} }
public struct CachedItemCollection { public struct CachedItemCollection {

View File

@ -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

View File

@ -403,7 +403,6 @@ private enum PeerInfoSettingsSection {
case avatar case avatar
case edit case edit
case proxy case proxy
case downloads
case savedMessages case savedMessages
case recentCalls case recentCalls
case devices 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: { items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_SavedMessages, icon: PresentationResourcesSettings.savedMessages, action: {
interaction.openSettings(.savedMessages) interaction.openSettings(.savedMessages)
})) }))
@ -5532,8 +5527,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil) self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
case .proxy: case .proxy:
self.controller?.push(proxySettingsController(context: self.context)) self.controller?.push(proxySettingsController(context: self.context))
case .downloads:
self.controller?.push(downloadsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData))
case .savedMessages: case .savedMessages:
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { 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))) self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.context.account.peerId)))

View File

@ -1128,6 +1128,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
navigateToChatControllerImpl(params) 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) { public func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) {
var found = false var found = false
for controller in navigationController.viewControllers.reversed() { for controller in navigationController.viewControllers.reversed() {