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