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

View File

@ -27,6 +27,8 @@ public enum ChatListSearchItemHeaderType {
case recentCalls
case orImportIntoAnExistingGroup
case subscribers
case downloading
case recentDownloads
fileprivate func title(strings: PresentationStrings) -> String {
switch self {
@ -74,6 +76,12 @@ public enum ChatListSearchItemHeaderType {
return strings.ChatList_HeaderImportIntoAnExistingGroup
case .subscribers:
return strings.Channel_ChannelSubscribersHeader
case .downloading:
//TODO:localize
return "Downloading"
case .recentDownloads:
//TODO:localize
return "Recently Downloaded"
}
}
@ -123,6 +131,10 @@ public enum ChatListSearchItemHeaderType {
return .orImportIntoAnExistingGroup
case .subscribers:
return .subscribers
case .downloading:
return .downloading
case .recentDownloads:
return .recentDownloads
}
}
}
@ -154,6 +166,8 @@ private enum ChatListSearchItemHeaderId: Int32 {
case recentCalls
case orImportIntoAnExistingGroup
case subscribers
case downloading
case recentDownloads
}
public final class ChatListSearchItemHeader: ListViewItemHeader {

View File

@ -110,7 +110,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { item, params, nextIsPinned, isEnabled in
return { [weak self] item, params, nextIsPinned, isEnabled in
let baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
@ -120,7 +120,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets())
return (layout, { [weak self] animated in
return (layout, { animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {

View File

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

View File

@ -25,6 +25,9 @@ import TooltipUI
import TelegramCallsUI
import StickerResources
import PasswordSetupUI
import FetchManagerImpl
import ComponentFlow
import LottieAnimationComponent
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
@ -151,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
private var activeDownloadsDisposable: Disposable?
private var didSetupTabs = false
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
@ -467,6 +472,95 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
self.searchContentNode?.updateExpansionProgress(0.0)
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
enum State {
case empty
case downloading
case hasUnseen
}
var stateSignal: Signal<State, NoError> = (combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox))
|> map { entries, recentDownloadItems -> State in
if !entries.isEmpty {
return .downloading
} else {
for item in recentDownloadItems {
if !item.isSeen {
return .hasUnseen
}
}
return .empty
}
}
|> mapToSignal { value -> Signal<State, NoError> in
return .single(value) |> delay(0.1, queue: .mainQueue())
}
|> distinctUntilChanged
|> deliverOnMainQueue)
if !"".isEmpty {
stateSignal = Signal<State, NoError>.single(.downloading)
|> then(Signal<State, NoError>.single(.hasUnseen) |> delay(3.0, queue: .mainQueue()))
}
self.activeDownloadsDisposable = stateSignal.start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
switch state {
case .downloading:
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
content: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.Animation(
name: "anim_search_downloading",
colors: [
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
],
loop: true
),
size: CGSize(width: 24.0, height: 24.0)
)),
insets: UIEdgeInsets(),
action: {
guard let strongSelf = self else {
return
}
strongSelf.activateSearch(filter: .downloads, query: nil)
}
)))
case .hasUnseen:
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
content: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.Animation(
name: "anim_search_downloaded",
colors: [
"Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
],
loop: false
),
size: CGSize(width: 24.0, height: 24.0)
)),
insets: UIEdgeInsets(),
action: {
guard let strongSelf = self else {
return
}
strongSelf.activateSearch(filter: .downloads, query: nil)
}
)))
case .empty:
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil)
}
})
}
if enableDebugActions {
@ -514,6 +608,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.stateDisposable.dispose()
self.filterDisposable.dispose()
self.featuredFiltersDisposable.dispose()
self.activeDownloadsDisposable?.dispose()
}
private func updateThemeAndStrings() {

View File

@ -29,6 +29,7 @@ import ChatInterfaceState
import ShareController
import UndoUI
import TextFormat
import Postbox
private enum ChatListTokenId: Int32 {
case archive
@ -45,14 +46,14 @@ final class ChatListSearchInteraction {
let clearRecentSearch: () -> Void
let addContact: (String) -> Void
let toggleMessageSelection: (EngineMessage.Id, Bool) -> Void
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)
let mediaMessageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
let peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
let present: (ViewController, Any?) -> Void
let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
self.openPeer = openPeer
self.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage
@ -112,8 +113,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var stateValue = ChatListSearchContainerNodeSearchState()
private let statePromise = ValuePromise<ChatListSearchContainerNodeSearchState>()
private var selectedFilterKey: ChatListSearchFilterEntryId?
private var selectedFilterKeyPromise = Promise<ChatListSearchFilterEntryId?>()
private var selectedFilter: ChatListSearchFilterEntry?
private var selectedFilterPromise = Promise<ChatListSearchFilterEntry?>()
private var transitionFraction: CGFloat = 0.0
private weak var copyProtectionTooltipController: TooltipController?
@ -134,8 +135,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.navigationController = navigationController
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.selectedFilterKey = .filter(initialFilter.id)
self.selectedFilterKeyPromise.set(.single(self.selectedFilterKey))
self.selectedFilter = .filter(initialFilter)
self.selectedFilterPromise.set(.single(self.selectedFilter))
self.openMessage = originalOpenMessage
self.present = present
@ -217,8 +218,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
return state.withUpdatedSelectedMessageIds(selectedMessageIds)
}
}
}, messageContextAction: { [weak self] message, node, rect, gesture in
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture)
}, messageContextAction: { [weak self] message, node, rect, gesture, paneKey, downloadResourceId in
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture, paneKey: paneKey, downloadResourceId: downloadResourceId)
}, mediaMessageContextAction: { [weak self] message, node, rect, gesture in
self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture)
}, peerContextAction: { peer, source, node, gesture in
@ -244,6 +245,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
filterKey = .chats
case .media:
filterKey = .media
case .downloads:
filterKey = .downloads
case .links:
filterKey = .links
case .files:
@ -253,8 +256,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
case .voice:
filterKey = .voice
}
strongSelf.selectedFilterKey = .filter(filterKey.id)
strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey))
strongSelf.selectedFilter = .filter(filterKey)
strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter))
strongSelf.transitionFraction = transitionFraction
if let (layout, _) = strongSelf.validLayout {
@ -262,9 +265,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters
} else {
filters = [.chats, .media, .links, .files, .music, .voice]
filters = [.chats, .media, .downloads, .links, .files, .music, .voice]
}
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
}
}
}
@ -283,6 +286,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .chats
case .media:
key = .media
case .downloads:
key = .downloads
case .links:
key = .links
case .files:
@ -306,7 +311,31 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.filterContainerNode.filterPressed?(initialFilter)
let suggestedPeers = self.searchQuery.get()
let searchQuerySignal = self.searchQuery.get()
let suggestedPeers = self.selectedFilterPromise.get()
|> map { filter -> Bool in
guard let filter = filter else {
return false
}
switch filter {
case let .filter(filter):
switch filter {
case .downloads:
return false
default:
return true
}
}
}
|> distinctUntilChanged
|> mapToSignal { value -> Signal<String?, NoError> in
if value {
return searchQuerySignal
} else {
return .single(nil)
}
}
|> mapToSignal { query -> Signal<[EnginePeer], NoError> in
if let query = query {
return context.account.postbox.searchPeers(query: query.lowercased())
@ -321,13 +350,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> take(1)
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterKeyPromise.get(), self.searchQuery.get(), accountPeer)
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer)
|> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in
if searchQuery?.isEmpty ?? true {
return .single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer)))
return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))
} else {
return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|> then(.single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer))))
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))))
}
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in
var suggestedFilters: [ChatListSearchFilter] = []
@ -523,6 +552,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music
case .voice:
key = .voice
case .downloads:
key = .downloads
default:
key = .chats
}
@ -553,7 +584,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}
let overflowInset: CGFloat = 20.0
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
var bottomIntrinsicInset = layout.intrinsicInsets.bottom
if case .root = self.groupId {
@ -737,10 +768,137 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let _ = self.paneContainerNode.scrollToTop()
}
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) {
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?, paneKey: ChatListSearchPaneKey, downloadResourceId: String?) {
guard let node = node as? ContextExtractedContentContainingNode else {
return
}
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
if paneKey == .downloads {
let isCachedValue: Signal<Bool, NoError>
if let downloadResourceId = downloadResourceId {
isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResourceId), resourceSize: nil)
|> map { status -> Bool in
switch status {
case .Local:
return true
default:
return false
}
}
|> distinctUntilChanged
} else {
isCachedValue = .single(false)
}
let shouldBeDismissed: Signal<Bool, NoError> = Signal { subscriber in
subscriber.putNext(false)
let previous = Atomic<Bool?>(value: nil)
return isCachedValue.start(next: { value in
let previousSwapped = previous.swap(value)
if let previousSwapped = previousSwapped, previousSwapped != value {
subscriber.putNext(true)
subscriber.putCompletion()
}
})
}
let items = combineLatest(queue: .mainQueue(),
context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: [message.id: message], peers: [:]),
isCachedValue |> take(1)
)
|> deliverOnMainQueue
|> map { [weak self] actions, isCachedValue -> [ContextMenuItem] in
guard let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
if isCachedValue {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Delete from Cache", textColor: .primary, icon: { _ in
return nil
}, action: { _, f in
guard let strongSelf = self, let downloadResourceId = downloadResourceId else {
f(.default)
return
}
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources([MediaResourceId(downloadResourceId)], notify: true)
|> deliverOnMainQueue).start(completed: {
f(.dismissWithoutContent)
})
})))
} else {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Cancel Downloading", textColor: .primary, icon: { _ in
return nil
}, action: { _, f in
guard let strongSelf = self, let downloadResourceId = downloadResourceId else {
f(.default)
return
}
let _ = strongSelf
let _ = downloadResourceId
f(.default)
})))
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: { [weak self] in
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false)
})
})))
if !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
if !items.isEmpty {
items.append(.separator)
}
if actions.options.contains(.deleteLocally) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, textColor: .destructive, icon: { _ in
return nil
}, action: { controller, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forLocalPeer).start()
f(.dismissWithoutContent)
})))
}
if actions.options.contains(.deleteGlobally) {
let text: String
if let mainPeer = message.peers[message.id.peerId] {
if mainPeer is TelegramUser {
text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(mainPeer).compactDisplayTitle).string
} else {
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
}
} else {
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
}
items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { _ in
return nil
}, action: { controller, f in
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).start()
f(.dismissWithoutContent)
})))
}
}
return items
}
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node, shouldBeDismissed: shouldBeDismissed)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture)
self.presentInGlobalOverlay?(controller, nil)
return
}
let _ = storedMessageFromSearch(account: self.context.account, message: message._asMessage()).start()
var linkForCopying: String?
@ -756,8 +914,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}
}
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
let context = self.context
let (peers, messages) = self.currentMessages
let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers)
@ -1150,10 +1306,13 @@ private final class MessageContextExtractedContentSource: ContextExtractedConten
let ignoreContentTouches: Bool = true
let blurBackground: Bool = true
let shouldBeDismissed: Signal<Bool, NoError>
private let sourceNode: ContextExtractedContentContainingNode
init(sourceNode: ContextExtractedContentContainingNode) {
init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal<Bool, NoError>? = nil) {
self.sourceNode = sourceNode
self.shouldBeDismissed = shouldBeDismissed ?? .single(false)
}
func takeView() -> ContextControllerTakeViewInfo? {

View File

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

View File

@ -26,6 +26,8 @@ import AppBundle
import ShimmerEffect
import ChatListSearchRecentPeersNode
import UndoUI
import Postbox
import FetchManagerImpl
private enum ChatListRecentEntryStableId: Hashable {
case topPeers
@ -217,7 +219,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
public enum ChatListSearchEntryStableId: Hashable {
case localPeerId(EnginePeer.Id)
case globalPeerId(EnginePeer.Id)
case messageId(EngineMessage.Id)
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
case addContact
}
@ -228,9 +230,54 @@ public enum ChatListSearchSectionExpandType {
}
public enum ChatListSearchEntry: Comparable, Identifiable {
public enum MessageOrderingKey: Comparable {
case index(MessageIndex)
case downloading(FetchManagerPriorityKey)
case downloaded(timestamp: Int32, index: MessageIndex)
public static func <(lhs: MessageOrderingKey, rhs: MessageOrderingKey) -> Bool {
switch lhs {
case let .index(lhsIndex):
if case let .index(rhsIndex) = rhs {
return lhsIndex > rhsIndex
} else {
return true
}
case let .downloading(lhsKey):
switch rhs {
case let .downloading(rhsKey):
return lhsKey > rhsKey
case .index:
return false
case .downloaded:
return true
}
case let .downloaded(lhsTimestamp, lhsIndex):
switch rhs {
case let .downloaded(rhsTimestamp, rhsIndex):
if lhsTimestamp != rhsTimestamp {
return lhsTimestamp > rhsTimestamp
} else {
return lhsIndex > rhsIndex
}
case .downloading:
return false
case .index:
return false
}
}
}
}
public enum MessageSection: Hashable {
case generic
case downloading
case recentlyDownloaded
}
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, String?, MessageSection)
case addContact(String, PresentationTheme, PresentationStrings)
public var stableId: ChatListSearchEntryStableId {
@ -239,8 +286,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _):
return .messageId(message.id)
case let .message(message, _, _, _, _, _, _, _, _, section):
return .messageId(message.id, section)
case .addContact:
return .addContact
}
@ -260,8 +307,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else {
return false
}
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader):
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = rhs {
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection):
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection) = rhs {
if lhsMessage.id != rhsMessage.id {
return false
}
@ -286,6 +333,15 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
if lhsDisplayCustomHeader != rhsDisplayCustomHeader {
return false
}
if lhsKey != rhsKey {
return false
}
if lhsResourceId != rhsResourceId {
return false
}
if lhsSection != rhsSection {
return false
}
return true
} else {
return false
@ -325,9 +381,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .message, .addContact:
return true
}
case let .message(lhsMessage, _, _, _, _, _, _):
if case let .message(rhsMessage, _, _, _, _, _, _) = rhs {
return lhsMessage.index < rhsMessage.index
case let .message(_, _, _, _, _, _, _, lhsKey, _, _):
if case let .message(_, _, _, _, _, _, _, rhsKey, _, _) = rhs {
return lhsKey < rhsKey
} else if case .addContact = rhs {
return true
} else {
@ -338,7 +394,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem {
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)?, openStorageSettings: @escaping () -> Void) -> ListViewItem {
switch self {
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType):
let primaryPeer: EnginePeer
@ -486,11 +542,29 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture)
}
})
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader):
let header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _):
let header: ChatListSearchItemHeader
switch orderingKey {
case .downloading:
//TODO:localize
header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Pause All", action: {})
case .downloaded:
//TODO:localize
header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Settings", action: {
openStorageSettings()
})
case .index:
header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
var isMedia = false
if let tagMask = tagMask, tagMask != .photoOrVideo {
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true)
isMedia = true
} else if key == .downloads {
isMedia = true
}
if isMedia {
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: key == .downloads ? header : nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: key != .downloads, isDownloadList: key == .downloads)
} else {
return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
}
@ -540,12 +614,12 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
}
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ChatListSearchContainerTransition {
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, String?) -> Void)?, openStorageSettings: @escaping () -> Void) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
}
@ -617,6 +691,25 @@ public struct ChatListSearchOptions {
}
}
private struct DownloadItem: Equatable {
let resourceId: MediaResourceId
let message: Message
let priority: FetchManagerPriorityKey
static func ==(lhs: DownloadItem, rhs: DownloadItem) -> Bool {
if lhs.resourceId != rhs.resourceId {
return false
}
if lhs.message.id != rhs.message.id {
return false
}
if lhs.priority != rhs.priority {
return false
}
return true
}
}
final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let context: AccountContext
private let interaction: ChatListSearchInteraction
@ -704,6 +797,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
tagMask = nil
case .media:
tagMask = .photoOrVideo
case .downloads:
tagMask = nil
case .links:
tagMask = .webPage
case .files:
@ -811,13 +906,120 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let presentationDataPromise = self.presentationDataPromise
let searchStatePromise = self.searchStatePromise
let selectionPromise = self.selectedMessagesPromise
let foundItems = combineLatest(searchQuery, searchOptions)
|> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
if query == nil && options == nil && tagMask == nil {
let downloadItems: Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError>
if key == .downloads {
downloadItems = combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox))
|> mapToSignal { entries, recentDownloadItems -> Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError> in
var itemSignals: [Signal<DownloadItem?, NoError>] = []
for entry in entries {
switch entry.id.locationKey {
case let .messageId(id):
itemSignals.append(context.account.postbox.transaction { transaction -> DownloadItem? in
if let message = transaction.getMessage(id) {
return DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority)
}
return nil
})
default:
break
}
}
return combineLatest(queue: .mainQueue(), itemSignals)
|> map { items -> (inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]) in
return (items.compactMap { $0 }, recentDownloadItems)
}
}
} else {
downloadItems = .single(([], []))
}
let foundItems = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems)
|> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
if query == nil && options == nil && key == .chats {
let _ = currentRemotePeers.swap(nil)
return .single(nil)
}
if key == .downloads {
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
func messageMatchesTokens(message: EngineMessage, tokens: [ValueBoxKey]) -> Bool {
for media in message.media {
if let file = media as? TelegramMediaFile {
if let fileName = file.fileName {
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {
return true
}
}
} else if let _ = media as? TelegramMediaImage {
if matchStringIndexTokens(stringIndexTokens("Photo Image", transliteration: .none), with: tokens) {
return true
}
}
}
return false
}
return presentationDataPromise.get()
|> map { presentationData -> ([ChatListSearchEntry], Bool)? in
var entries: [ChatListSearchEntry] = []
var existingMessageIds = Set<MessageId>()
for item in downloadItems.inProgressItems {
if existingMessageIds.contains(item.message.id) {
continue
}
existingMessageIds.insert(item.message.id)
let message = EngineMessage(item.message)
if !queryTokens.isEmpty {
if !messageMatchesTokens(message: message, tokens: queryTokens) {
continue
}
}
var peer = EngineRenderedPeer(message: message)
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
if let channelPeer = message.peers[migrationReference.peerId] {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
}
}
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloading(item.priority), item.resourceId.stringRepresentation, .downloading))
}
for item in downloadItems.doneItems {
if !item.isSeen {
Queue.mainQueue().async {
self?.scheduleMarkRecentDownloadsAsSeen()
}
}
if existingMessageIds.contains(item.message.id) {
continue
}
existingMessageIds.insert(item.message.id)
let message = EngineMessage(item.message)
if !queryTokens.isEmpty {
if !messageMatchesTokens(message: message, tokens: queryTokens) {
continue
}
}
var peer = EngineRenderedPeer(message: message)
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
if let channelPeer = message.peers[migrationReference.peerId] {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
}
}
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloaded(timestamp: item.timestamp, index: message.index), item.resourceId, .recentlyDownloaded))
}
return (entries.sorted(), false)
}
}
let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1)
let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError>
if let query = query {
@ -1108,7 +1310,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
}
}
entries.append(.message(message, peer, nil, presentationData, 1, nil, true))
entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic))
index += 1
}
@ -1131,7 +1333,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
}
}
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId))
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic))
index += 1
}
}
@ -1249,8 +1451,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
})
let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in
guard let strongSelf = self else {
return false
}
interaction.dismissInput()
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
let gallerySource: GalleryControllerItemSource
if strongSelf.key == .downloads {
gallerySource = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil))
} else {
gallerySource = .custom(messages: foundMessages |> map { message, a, b in
return (message.map { $0._asMessage() }, a, b)
}, messageId: message.id, loadMore: {
loadMore()
})
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
interaction.dismissInput()
}, present: { c, a in
interaction.present(c, a)
@ -1277,13 +1495,25 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return (message.map { $0._asMessage() }, a, b)
}, at: message.id, loadMore: {
loadMore()
}), gallerySource: .custom(messages: foundMessages |> map { message, a, b in
return (message.map { $0._asMessage() }, a, b)
}, messageId: message.id, loadMore: {
loadMore()
})))
}, openMessageContextMenu: { message, _, node, rect, gesture in
interaction.messageContextAction(EngineMessage(message), node, rect, gesture)
}), gallerySource: gallerySource))
}, openMessageContextMenu: { [weak self] message, _, node, rect, gesture in
guard let strongSelf = self, let currentEntries = strongSelf.currentEntries else {
return
}
var fetchResourceId: String?
for entry in currentEntries {
switch entry {
case let .message(m, _, _, _, _, _, _, _, resourceId, _):
if m.id == message.id {
fetchResourceId = resourceId
}
default:
break
}
}
interaction.messageContextAction(EngineMessage(message), node, rect, gesture, key, fetchResourceId)
}, toggleMessagesSelection: { messageId, selected in
if let messageId = messageId.first {
interaction.toggleMessageSelection(messageId, selected)
@ -1355,7 +1585,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
let firstTime = previousEntries == nil
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
interaction.peerContextAction?(message, node, rect, gesture)
}, toggleExpandLocalResults: { [weak self] in
guard let strongSelf = self else {
@ -1376,15 +1606,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return state
}
}, searchPeer: { peer in
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in
interaction.messageContextAction(message, node, rect, gesture)
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResourceId in
interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResourceId)
}, openStorageSettings: {
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context)
})
strongSelf.currentEntries = newEntries
strongSelf.enqueueTransition(transition, firstTime: firstTime)
var messages: [EngineMessage] = []
for entry in newEntries {
if case let .message(message, _, _, _, _, _, _) = entry {
if case let .message(message, _, _, _, _, _, _, _, _, _) = entry {
messages.append(message)
}
}
@ -1652,6 +1887,27 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
func didBecomeFocused() {
if self.key == .downloads {
self.scheduleMarkRecentDownloadsAsSeen()
}
}
private var scheduledMarkRecentDownloadsAsSeen: Bool = false
func scheduleMarkRecentDownloadsAsSeen() {
if !self.scheduledMarkRecentDownloadsAsSeen {
self.scheduledMarkRecentDownloadsAsSeen = true
Queue.mainQueue().after(0.1, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.scheduledMarkRecentDownloadsAsSeen = false
let _ = markAllRecentDownloadItemsAsSeen(postbox: strongSelf.context.account.postbox).start()
})
}
}
func scrollToTop() -> Bool {
if !self.mediaNode.isHidden {
return self.mediaNode.scrollToTop()
@ -2110,16 +2366,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
}
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
if strongSelf.key == .downloads {
strongSelf.emptyResultsAnimationNode.isHidden = true
strongSelf.emptyResultsTitleNode.isHidden = true
strongSelf.emptyResultsTextNode.isHidden = true
strongSelf.emptyResultsAnimationNode.visibility = false
} else {
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
}
let displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
var displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
if strongSelf.key == .downloads {
displayPlaceholder = false
}
let targetAlpha: CGFloat = displayPlaceholder ? 1.0 : 0.0
if strongSelf.shimmerNode.alpha != targetAlpha {
let transition: ContainedViewLayoutTransition = displayPlaceholder ? .immediate : .animated(duration: 0.2, curve: .linear)
let transition: ContainedViewLayoutTransition = (displayPlaceholder || isFirstTime) ? .immediate : .animated(duration: 0.2, curve: .linear)
transition.updateAlpha(node: strongSelf.shimmerNode, alpha: targetAlpha, delay: 0.1)
}
@ -2334,7 +2600,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
switch key {
case .chats:
case .chats, .downloads:
let message = EngineMessage(
stableId: 0,
stableVersion: 0,
@ -2358,7 +2624,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
associatedMessageIds: []
)
let readState = EnginePeerReadCounters()
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
case .media:
return nil
case .links:

View File

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

View File

@ -19,6 +19,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode {
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
func didBecomeFocused()
var searchCurrentMessages: [EngineMessage]? { get }
}
@ -44,16 +45,17 @@ final class ChatListSearchPaneWrapper {
}
}
enum ChatListSearchPaneKey {
public enum ChatListSearchPaneKey {
case chats
case media
case downloads
case links
case files
case music
case voice
}
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice]
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice]
struct ChatListSearchPaneSpecifier: Equatable {
var key: ChatListSearchPaneKey
@ -475,6 +477,9 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
})
}
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
if paneWasAdded && key == self.currentPaneKey {
pane.node.didBecomeFocused()
}
}
}

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>()
var elevatedPriorityReferenceCount: Int32 = 0
var userInitiatedPriorityIndices: [Int32] = []
var isPaused: Bool = false
var combinedRanges: IndexSet {
var result = IndexSet()
@ -116,7 +117,7 @@ private final class FetchManagerCategoryContext {
private let postbox: Postbox
private let storeManager: DownloadedMediaStoreManager?
private let entryCompleted: (FetchManagerLocationEntryId) -> Void
private let activeEntriesUpdated: ([FetchManagerEntrySummary]) -> Void
private let activeEntriesUpdated: () -> Void
private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)?
private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:]
@ -133,13 +134,17 @@ private final class FetchManagerCategoryContext {
return false
}
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping ([FetchManagerEntrySummary]) -> Void) {
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping () -> Void) {
self.postbox = postbox
self.storeManager = storeManager
self.entryCompleted = entryCompleted
self.activeEntriesUpdated = activeEntriesUpdated
}
func getActiveEntries() -> [FetchManagerLocationEntry] {
return Array(self.entries.values)
}
func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (AnyMediaReference?, MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) {
let entry: FetchManagerLocationEntry
let previousPriorityKey: FetchManagerPriorityKey?
@ -221,8 +226,13 @@ private final class FetchManagerCategoryContext {
parsedRanges = resultRanges
}
activeContext.disposable?.dispose()
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
let postbox = self.postbox
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
}
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> castError(FetchResourceError.self)
@ -286,7 +296,7 @@ private final class FetchManagerCategoryContext {
}
}
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
self.activeEntriesUpdated()
}
func maybeFindAndActivateNewTopEntry() -> Bool {
@ -366,8 +376,13 @@ private final class FetchManagerCategoryContext {
})
} else if ranges.isEmpty {
} else {
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
let postbox = self.postbox
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
}
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|> castError(FetchResourceError.self)
@ -446,7 +461,7 @@ private final class FetchManagerCategoryContext {
activeContextsUpdated = true
}
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
self.activeEntriesUpdated()
}
func withFetchStatusContext(_ id: FetchManagerLocationEntryId, _ f: (FetchManagerStatusContext) -> Void) {
@ -493,6 +508,37 @@ private extension FetchManagerEntrySummary {
}
}
private func filterDownloadStatsEntry(entry: FetchManagerLocationEntry) -> Bool {
guard let mediaReference = entry.mediaReference else {
return false
}
if !entry.userInitiated {
return false
}
switch mediaReference {
case let .message(_, media):
switch media {
case let file as TelegramMediaFile:
if file.isVideo {
if file.isAnimated {
return false
}
}
if file.isSticker {
return false
}
if file.isVoice {
return false
}
return true
default:
return false
}
default:
return false
}
}
public final class FetchManagerImpl: FetchManager {
public let queue = Queue.mainQueue()
private let postbox: Postbox
@ -545,21 +591,26 @@ public final class FetchManagerImpl: FetchManager {
context.cancelEntry(id, isCompleted: true)
})
}
}, activeEntriesUpdated: { [weak self] entries in
}, activeEntriesUpdated: { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
var hasActiveUserInitiatedEntries = false
var activeEntries: [FetchManagerEntrySummary] = []
for (_, context) in strongSelf.categoryContexts {
if context.hasActiveUserInitiatedEntries {
hasActiveUserInitiatedEntries = true
break
}
activeEntries.append(contentsOf: context.getActiveEntries().filter { entry in
return filterDownloadStatsEntry(entry: entry)
}.compactMap { entry -> FetchManagerEntrySummary? in
return FetchManagerEntrySummary(entry: entry)
})
}
strongSelf.hasUserInitiatedEntriesValue.set(hasActiveUserInitiatedEntries)
strongSelf.entriesSummaryValue.set(entries)
strongSelf.entriesSummaryValue.set(activeEntries.sorted(by: { $0.priority < $1.priority }))
}
})
self.categoryContexts[key] = context

View File

@ -45,7 +45,7 @@ public final class HashtagSearchController: TelegramBaseController {
|> map { result, presentationData in
let result = result.0
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false) })
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic) })
}
let interaction = ChatListNodeInteraction(activateSearch: {
}, peerSelected: { _, _, _ in
@ -95,10 +95,10 @@ public final class HashtagSearchController: TelegramBaseController {
})
let firstTime = previousEntries == nil
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
}, toggleExpandGlobalResults: {
}, searchPeer: { _ in
}, searchQuery: "", searchOptions: nil, messageContextAction: nil)
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {})
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
}
})

View File

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

View File

@ -51,13 +51,14 @@ public final class ListMessageItem: ListViewItem {
public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool
let isGlobalSearchResult: Bool
let isDownloadList: Bool
let displayBackground: Bool
let header: ListViewItemHeader?
public let selectable: Bool = true
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, displayBackground: Bool = false) {
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayBackground: Bool = false) {
self.presentationData = presentationData
self.context = context
self.chatLocation = chatLocation
@ -73,6 +74,7 @@ public final class ListMessageItem: ListViewItem {
self.selection = selection
self.hintIsLink = hintIsLink
self.isGlobalSearchResult = isGlobalSearchResult
self.isDownloadList = isDownloadList
self.displayBackground = displayBackground
}

View File

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

View File

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

View File

@ -1053,9 +1053,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
self.textBackgroundNode.isHidden = true
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak node] _ in
textBackgroundCompleted = true
intermediateCompletion()
if let node = node, let accessoryComponentView = node.accessoryComponentView {
accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
accessoryComponentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
}
})
let transitionBackgroundNode = ASDisplayNode()

View File

@ -4,6 +4,7 @@ import SwiftSignalKit
import AsyncDisplayKit
import Display
import AppBundle
import ComponentFlow
private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe")
@ -35,6 +36,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
public private(set) var placeholderString: NSAttributedString?
var accessoryComponentView: ComponentHostView<Empty>?
convenience public override init() {
self.init(fieldStyle: .legacy)
}
@ -105,6 +108,29 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
})
}
public func setAccessoryComponent(component: AnyComponent<Empty>?) {
if let component = component {
let accessoryComponentView: ComponentHostView<Empty>
if let current = self.accessoryComponentView {
accessoryComponentView = current
} else {
accessoryComponentView = ComponentHostView()
self.accessoryComponentView = accessoryComponentView
self.view.addSubview(accessoryComponentView)
}
let accessorySize = accessoryComponentView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
accessoryComponentView.frame = CGRect(origin: CGPoint(x: self.bounds.width - accessorySize.width - 4.0, y: floor((self.bounds.height - accessorySize.height) / 2.0)), size: accessorySize)
} else if let accessoryComponentView = self.accessoryComponentView {
self.accessoryComponentView = nil
accessoryComponentView.removeFromSuperview()
}
}
public func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ expansionProgress: CGFloat, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor, _ transition: ContainedViewLayoutTransition) -> (CGFloat, () -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
let currentForegroundColor = self.foregroundColor
@ -183,6 +209,10 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)))
if let accessoryComponentView = strongSelf.accessoryComponentView {
accessoryComponentView.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentView.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentView.bounds.height) / 2.0)), size: accessoryComponentView.bounds.size)
}
}
})
}

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 CloudThemes: Int32 = 9
public static let CloudGreetingStickers: Int32 = 10
public static let RecentDownloads: Int32 = 11
}
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 edit
case proxy
case downloads
case savedMessages
case recentCalls
case devices
@ -663,10 +662,6 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}
}
//TODO:localize
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "Downloads", icon: PresentationResourcesSettings.savedMessages, action: {
interaction.openSettings(.downloads)
}))
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_SavedMessages, icon: PresentationResourcesSettings.savedMessages, action: {
interaction.openSettings(.savedMessages)
}))
@ -5532,8 +5527,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
case .proxy:
self.controller?.push(proxySettingsController(context: self.context))
case .downloads:
self.controller?.push(downloadsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData))
case .savedMessages:
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.context.account.peerId)))

View File

@ -1128,6 +1128,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
navigateToChatControllerImpl(params)
}
public func openStorageUsage(context: AccountContext) {
guard let navigationController = self.mainWindow?.viewController as? NavigationController else {
return
}
let controller = storageUsageController(context: context, isModal: true)
navigationController.pushViewController(controller)
}
public func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) {
var found = false
for controller in navigationController.viewControllers.reversed() {