From 8086ea1ecb5c6cda95116d2884f54dcbd12ea99a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 22 Feb 2022 22:08:42 +0400 Subject: [PATCH] Download list improvements --- .../AccountContext/Sources/MediaManager.swift | 6 +- .../Sources/ChatListSearchItem.swift | 3 +- .../Sources/ChatListController.swift | 18 +- .../Sources/ChatListControllerNode.swift | 6 +- .../Sources/ChatListSearchContainerNode.swift | 188 +++++++++++++----- .../Sources/ChatListSearchListPaneNode.swift | 77 +++++-- .../Sources/FileMediaResourceStatus.swift | 17 +- .../Sources/HashtagSearchController.swift | 2 +- .../Sources/ListMessageFileItemNode.swift | 98 +++++---- .../Sources/SearchBarPlaceholderNode.swift | 11 +- .../NavigationBarSearchContentNode.swift | 13 +- .../Sources/SearchDisplayController.swift | 6 +- .../NotificationSearchItem.swift | 3 +- .../SyncCore_RecentDownloadItem.swift | 7 + .../ChatMessageInteractiveFileNode.swift | 18 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- 16 files changed, 323 insertions(+), 152 deletions(-) diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 35d6a5bcda..1b4e87c34a 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -116,10 +116,10 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer return nil } -public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { - if isGlobalSearch { +public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { + if isGlobalSearch && !isDownloadList { return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) - } else if isRecentActions { + } else if isRecentActions && !isDownloadList { return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else { return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) diff --git a/submodules/ChatListSearchItemNode/Sources/ChatListSearchItem.swift b/submodules/ChatListSearchItemNode/Sources/ChatListSearchItem.swift index 24b3edf898..d2959cd0f3 100644 --- a/submodules/ChatListSearchItemNode/Sources/ChatListSearchItem.swift +++ b/submodules/ChatListSearchItemNode/Sources/ChatListSearchItem.swift @@ -116,7 +116,8 @@ public class ChatListSearchItemNode: ListViewItemNode { let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor let placeholderColor = item.theme.list.itemSecondaryTextColor - let (_, searchBarApply) = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: placeholderColor), CGSize(width: baseWidth - 20.0, height: 36.0), 1.0, placeholderColor, nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate) + let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: placeholderColor) + let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 20.0, height: 36.0), 1.0, placeholderColor, nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets()) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index bcc2d1fb7c..61068fd483 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -469,7 +469,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) if !previewing { - self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel, activate: { [weak self] in + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel, compactPlaceholder: self.presentationData.strings.DialogList_SearchLabelCompact, activate: { [weak self] in self?.activateSearch() }) self.searchContentNode?.updateExpansionProgress(0.0) @@ -601,9 +601,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let stateSignal: Signal = (combineLatest(queue: .mainQueue(), entriesWithFetchStatuses, recentDownloadItems(postbox: context.account.postbox)) - |> map { entries, recentDownloadItems -> State in - if !entries.isEmpty { + let displayRecentDownloads = context.account.postbox.tailChatListView(groupId: .root, filterPredicate: nil, count: 11, summaryComponents: ChatListEntrySummaryComponents(components: [:])) + |> map { view -> Bool in + return view.0.entries.count >= 10 + } + |> distinctUntilChanged + + let stateSignal: Signal = (combineLatest(queue: .mainQueue(), entriesWithFetchStatuses, recentDownloadItems(postbox: context.account.postbox), displayRecentDownloads) + |> map { entries, recentDownloadItems, displayRecentDownloads -> State in + if !entries.isEmpty && displayRecentDownloads { var totalBytes = 0.0 var totalProgressInBytes = 0.0 for (entry, progress) in entries { @@ -799,7 +805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.navigationItem.backBarButtonItem = backBarButtonItem } - self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel, compactPlaceholder: self.presentationData.strings.DialogList_SearchLabelCompact) let editing = self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing let editItem: UIBarButtonItem if editing { @@ -2088,7 +2094,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - activate() + activate(filter != .downloads) if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { searchContentNode.search(filter: filter, query: query) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 32410e70a6..dd9cb8f4d5 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1193,7 +1193,7 @@ final class ChatListControllerNode: ASDisplayNode { } } - func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, () -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return nil } @@ -1226,7 +1226,7 @@ final class ChatListControllerNode: ASDisplayNode { }) self.containerNode.accessibilityElementsHidden = true - return (contentNode.filterContainerNode, { [weak self] in + return (contentNode.filterContainerNode, { [weak self] focus in guard let strongSelf = self else { return } @@ -1239,7 +1239,7 @@ final class ChatListControllerNode: ASDisplayNode { strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) } } - }, placeholder: placeholderNode) + }, placeholder: placeholderNode, focus: focus) }) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index f6170af27f..9247c46a5a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -870,7 +870,28 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) }))) - if !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { + if isCachedValue { + if !items.isEmpty { + items.append(.separator) + } + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { + if let strongSelf = self { + strongSelf.dismissInput() + + strongSelf.updateState { state in + return state.withUpdatedSelectedMessageIds([message.id]) + } + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + }) + }))) + } + + /*if !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { if !items.isEmpty { items.append(.separator) } @@ -905,7 +926,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo f(.dismissWithoutContent) }))) } - } + }*/ return items } @@ -1061,34 +1082,53 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } func deleteMessages(messageIds: Set?) { + let isDownloads = self.paneContainerNode.currentPaneKey == .downloads + if let messageIds = messageIds ?? self.stateValue.selectedMessageIds, !messageIds.isEmpty { - let (peers, messages) = self.currentMessages - let _ = (self.context.account.postbox.transaction { transaction -> Void in - for id in messageIds { - if transaction.getMessage(id) == nil, let message = messages[id] { - storeMessageFromSearch(transaction: transaction, message: message._asMessage()) - } + if isDownloads { + let _ = (self.context.account.postbox.transaction { transaction -> [Message] in + return messageIds.compactMap(transaction.getMessage) } - }).start() - - self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds, messages: messages, peers: peers) - |> deliverOnMainQueue).start(next: { [weak self] actions in - if let strongSelf = self, !actions.options.isEmpty { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - let personalPeerName: String? = nil + |> deliverOnMainQueue).start(next: { [weak self] messages in + guard let strongSelf = self else { + return + } - if actions.options.contains(.deleteGlobally) { - let globalTitle: String - if let personalPeerName = personalPeerName { - globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string - } else { - globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone - } - items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() + let title: String + let text: String + + //TODO:localize + if messageIds.count == 1 { + title = "Remove Document?" + text = "Are you sure you want to remove this\ndocument from Downloads?\nIt will be deleted from your disk, but\nwill remain accessible in the cloud."; + } else { + title = "Remove \(messages.count) Documents?" + text = "Do you want to remove these\n\(messages.count) documents from Downloads?\nThey will be deleted from your disk,\nbut will remain accessible\nin the cloud." + } + + strongSelf.present?(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + //TODO:localize + TextAlertAction(type: .defaultAction, title: "Remove", action: { + guard let strongSelf = self else { + return + } + + var resourceIds = Set() + for message in messages { + for media in message.media { + if let file = media as? TelegramMediaFile { + resourceIds.insert(file.resource.id) + } + } + } + + let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(resourceIds, force: true, notify: true) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) @@ -1096,34 +1136,74 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) } - } - })) - } - if actions.options.contains(.deleteLocally) { - let localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe - items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() - - strongSelf.updateState { state in - return state.withUpdatedSelectedMessageIds(nil) - } - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + }) }) - ])]) - strongSelf.view.endEditing(true) - strongSelf.present?(actionSheet, nil) - } - })) + ], actionLayout: .horizontal, parseMarkdown: true), nil) + }) + } else { + let (peers, messages) = self.currentMessages + let _ = (self.context.account.postbox.transaction { transaction -> Void in + for id in messageIds { + if transaction.getMessage(id) == nil, let message = messages[id] { + storeMessageFromSearch(transaction: transaction, message: message._asMessage()) + } + } + }).start() + + self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds, messages: messages, peers: peers) + |> deliverOnMainQueue).start(next: { [weak self] actions in + if let strongSelf = self, !actions.options.isEmpty { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + let personalPeerName: String? = nil + + if actions.options.contains(.deleteGlobally) { + let globalTitle: String + if let personalPeerName = personalPeerName { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string + } else { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone + } + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() + + strongSelf.updateState { state in + return state.withUpdatedSelectedMessageIds(nil) + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + })) + } + if actions.options.contains(.deleteLocally) { + let localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe + items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() + + strongSelf.updateState { state in + return state.withUpdatedSelectedMessageIds(nil) + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.view.endEditing(true) + strongSelf.present?(actionSheet, nil) + } + })) + } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 811935d1df..a4626c788d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -400,7 +400,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - 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, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openStorageSettings: @escaping () -> Void, toggleAllPaused: @escaping () -> 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, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ListViewItem { switch self { case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): let primaryPeer: EnginePeer @@ -564,8 +564,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } case .downloaded: //TODO:localize - header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Settings", action: { - openStorageSettings() + header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Clear", action: { + openClearRecentlyDownloaded() }) case .index: header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) @@ -628,12 +628,12 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, 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, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openStorageSettings: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, 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, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings, toggleAllPaused: toggleAllPaused), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings, toggleAllPaused: toggleAllPaused), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -981,8 +981,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return false } - return presentationDataPromise.get() - |> map { presentationData -> ([ChatListSearchEntry], Bool)? in + return combineLatest(queue: .mainQueue(), presentationDataPromise.get(), selectionPromise.get()) + |> map { presentationData, selectionState -> ([ChatListSearchEntry], Bool)? in var entries: [ChatListSearchEntry] = [] var existingMessageIds = Set() @@ -1047,7 +1047,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) } } - entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false)) + + entries.append(.message(message, peer, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false)) } return (entries.sorted(), false) } @@ -1501,6 +1502,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) } + let playlistLocation: PeerMessagesPlaylistLocation? + if strongSelf.key == .downloads { + playlistLocation = nil + } else { + playlistLocation = .custom(messages: foundMessages |> map { message, a, b in + return (message.map { $0._asMessage() }, a, b) + }, at: message.id, loadMore: { + loadMore() + }) + } + return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { interaction.dismissInput() }, present: { c, a in @@ -1524,11 +1536,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPeer: { peer, navigation in }, callPeer: { _, _ in }, enqueueMessage: { _ in - }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .custom(messages: foundMessages |> map { message, a, b in - return (message.map { $0._asMessage() }, a, b) - }, at: message.id, loadMore: { - loadMore() - }), gallerySource: gallerySource)) + }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation, gallerySource: gallerySource)) }, openMessageContextMenu: { [weak self] message, _, node, rect, gesture in guard let strongSelf = self, let currentEntries = strongSelf.currentEntries else { return @@ -1641,11 +1649,44 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, searchPeer: { peer in }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResource) - }, openStorageSettings: { + }, openClearRecentlyDownloaded: { guard let strongSelf = self else { return } - strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context) + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + + //TODO:localize + items.append(ActionSheetTextItem(title: "Telegram allows to store all received and sent\ndocuments in the cloud and save storage\nspace on your device.")) + + //TODO:localize + items.append(ActionSheetButtonItem(title: "Manage Device Storage", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + + strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context) + })) + + //TODO:localize + items.append(ActionSheetButtonItem(title: "Clear Downloads List", color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + + let _ = clearRecentDownloadList(postbox: strongSelf.context.account.postbox).start() + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.interaction.dismissInput() + strongSelf.interaction.present(actionSheet, nil) }, toggleAllPaused: { guard let strongSelf = self else { return @@ -1833,7 +1874,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { loadMore() } - if [.file, .music, .voiceOrInstantVideo].contains(tagMask) { + if [.file, .music, .voiceOrInstantVideo].contains(tagMask) || self.key == .downloads { self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState |> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in if let (account, state, type) = playlistStateAndType { @@ -1846,7 +1887,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return .single(nil) |> delay(0.2, queue: .mainQueue()) } case .music: - if tagMask != .music { + if tagMask != .music && self.key != .downloads { return .single(nil) |> delay(0.2, queue: .mainQueue()) } case .file: diff --git a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift index e514d49fca..16d997708d 100644 --- a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift +++ b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift @@ -6,12 +6,12 @@ import SwiftSignalKit import UniversalMediaPlayer import AccountContext -private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal { +private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { guard let playerType = peerMessageMediaPlayerType(message) else { return .single(nil) } - if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) { + if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) { return context.sharedContext.mediaManager.filteredPlaylistState(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType) |> mapToSignal { state -> Signal in return .single(state?.status) @@ -21,31 +21,32 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil } } -public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal { +public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { var duration = 0.0 if let value = file.duration { duration = Double(value) } let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) - return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) |> map { status in + return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) + |> map { status in return status ?? defaultStatus } } -public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal { +public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { guard let playerType = peerMessageMediaPlayerType(message) else { return .never() } - if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) { + if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) { return context.sharedContext.mediaManager.filteredPlayerAudioLevelEvents(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType) } else { return .never() } } -public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal { - let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) |> map { status -> MediaPlayerPlaybackStatus? in +public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal { + let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in return status?.status } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 537e40c918..ce53a3b5bc 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -98,7 +98,7 @@ public final class HashtagSearchController: TelegramBaseController { let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { }, toggleExpandGlobalResults: { }, searchPeer: { _ in - }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {}, toggleAllPaused: {}) + }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {}) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 0fc90e78f8..f5cf5d6892 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -496,7 +496,10 @@ public final class ListMessageFileItemNode: ListMessageNode { descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) iconImage = .roundVideo(file) } else if !isAudio { - let fileName: String = file.fileName ?? "File" + var fileName: String = file.fileName ?? "File" + if file.isVideo { + fileName = item.presentationData.strings.Message_Video + } titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) var fileExtension: String? @@ -543,8 +546,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } else if let image = media as? TelegramMediaImage { selectedMedia = image - //TODO:localize - let fileName: String = "Photo" + let fileName: String = item.presentationData.strings.Message_Video titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) if let representation = smallestImageRepresentation(image.representations) { @@ -624,7 +626,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if statusUpdated { if let file = selectedMedia as? TelegramMediaFile { - updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) + updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) |> mapToSignal { value -> Signal in if case .Fetching = value.fetchStatus { return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) @@ -638,16 +640,20 @@ public final class ListMessageFileItemNode: ListMessageNode { updatedStatusSignal = currentUpdatedStatusSignal |> map { status in switch status.mediaStatus { - case .fetchStatus: + case .fetchStatus: + if item.isDownloadList { + return FileMediaResourceStatus(mediaStatus: .fetchStatus(status.fetchStatus), fetchStatus: status.fetchStatus) + } else { return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus) - case .playbackStatus: - return status + } + case .playbackStatus: + return status } } } } if isVoice { - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) } } else if let image = selectedMedia as? TelegramMediaImage { updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) @@ -996,27 +1002,32 @@ public final class ListMessageFileItemNode: ListMessageNode { if !isAudio && !isInstantVideo { self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) } else { + if item.isDownloadList { + self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) + } switch status { - case let .fetchStatus(fetchStatus): - switch fetchStatus { - case .Fetching: - break - case .Local: - if isAudio || isInstantVideo { - iconStatusState = .play - } - case .Remote, .Paused: - if isAudio || isInstantVideo { - iconStatusState = .play - } + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case let .Fetching(_, progress): + if item.isDownloadList { + iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: false, appearance: nil) } - case let .playbackStatus(playbackStatus): - switch playbackStatus { - case .playing: - iconStatusState = .pause - case .paused: + case .Local: + if isAudio || isInstantVideo { iconStatusState = .play } + case .Remote, .Paused: + if isAudio || isInstantVideo { + iconStatusState = .play + } + } + case let .playbackStatus(playbackStatus): + switch playbackStatus { + case .playing: + iconStatusState = .pause + case .paused: + iconStatusState = .play + } } } self.iconStatusNode.backgroundNodeColor = iconStatusBackgroundColor @@ -1090,17 +1101,22 @@ public final class ListMessageFileItemNode: ListMessageNode { break case let .fetchStatus(fetchStatus): maybeFetchStatus = fetchStatus - switch fetchStatus { - case .Fetching(_, let progress), .Paused(let progress): - if let file = self.currentMedia as? TelegramMediaFile, let size = file.size { - downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))" - } - descriptionOffset = 14.0 - case .Remote: - descriptionOffset = 14.0 - case .Local: - break - } + } + + if item.isDownloadList, let fetchStatus = self.fetchStatus { + maybeFetchStatus = fetchStatus + } + + switch maybeFetchStatus { + case .Fetching(_, let progress), .Paused(let progress): + if let file = self.currentMedia as? TelegramMediaFile, let size = file.size { + downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))" + } + descriptionOffset = 14.0 + case .Remote: + descriptionOffset = 14.0 + case .Local: + break } switch maybeFetchStatus { @@ -1163,7 +1179,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } else { if let linearProgressNode = self.linearProgressNode { self.linearProgressNode = nil - linearProgressNode.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in + linearProgressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in linearProgressNode?.removeFromSupernode() }) } @@ -1419,9 +1435,11 @@ private final class DownloadIconNode: ASImageNode { } func updateTheme(theme: PresentationTheme) { - self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: { - return generateDownloadIcon(color: theme.list.itemAccentColor) - }) + if self.image != nil { + self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: { + return generateDownloadIcon(color: theme.list.itemAccentColor) + }) + } self.customColor = theme.list.itemAccentColor self.animationNode?.customColor = self.customColor } diff --git a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index 9b557c036d..d4b6c22355 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -134,12 +134,19 @@ public class SearchBarPlaceholderNode: ASDisplayNode { } } - public func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ expansionProgress: CGFloat, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor, _ transition: ContainedViewLayoutTransition) -> (CGFloat, () -> Void) { + public func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ compactPlaceholderString: NSAttributedString?, _ constrainedSize: CGSize, _ expansionProgress: CGFloat, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor, _ transition: ContainedViewLayoutTransition) -> (CGFloat, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let currentForegroundColor = self.foregroundColor let currentIconColor = self.iconColor - return { placeholderString, constrainedSize, expansionProgress, iconColor, foregroundColor, backgroundColor, transition in + return { fullPlaceholderString, compactPlaceholderString, constrainedSize, expansionProgress, iconColor, foregroundColor, backgroundColor, transition in + let placeholderString: NSAttributedString? + if constrainedSize.width < 350.0 { + placeholderString = compactPlaceholderString + } else { + placeholderString = fullPlaceholderString + } + let (labelLayoutResult, labelApply) = labelLayout(TextNodeLayoutArguments(attributedString: placeholderString, backgroundColor: .clear, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var updatedColor: UIColor? diff --git a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift index a2bcfca7b0..1021bf0961 100644 --- a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift +++ b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift @@ -11,6 +11,7 @@ public let navigationBarSearchContentHeight: CGFloat = 54.0 public class NavigationBarSearchContentNode: NavigationBarContentNode { public var theme: PresentationTheme? public var placeholder: String + public var compactPlaceholder: String public let placeholderNode: SearchBarPlaceholderNode public var placeholderHeight: CGFloat? @@ -20,9 +21,10 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { private var validLayout: (CGSize, CGFloat, CGFloat)? - public init(theme: PresentationTheme, placeholder: String, activate: @escaping () -> Void) { + public init(theme: PresentationTheme, placeholder: String, compactPlaceholder: String? = nil, activate: @escaping () -> Void) { self.theme = theme self.placeholder = placeholder + self.compactPlaceholder = compactPlaceholder ?? placeholder self.placeholderNode = SearchBarPlaceholderNode(fieldStyle: .modern) self.placeholderNode.labelNode.displaysAsynchronously = false @@ -36,9 +38,10 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { self.placeholderNode.activate = activate } - public func updateThemeAndPlaceholder(theme: PresentationTheme, placeholder: String) { + public func updateThemeAndPlaceholder(theme: PresentationTheme, placeholder: String, compactPlaceholder: String? = nil) { self.theme = theme self.placeholder = placeholder + self.compactPlaceholder = compactPlaceholder ?? placeholder self.placeholderNode.accessibilityLabel = placeholder if let disabledOverlay = self.disabledOverlay { disabledOverlay.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor.withAlphaComponent(0.5) @@ -105,7 +108,11 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { let overscrollProgress = max(0.0, max(0.0, self.expansionProgress - 1.0 + fraction) / fraction - visibleProgress) let searchBarNodeLayout = self.placeholderNode.asyncLayout() - let (searchBarHeight, searchBarApply) = searchBarNodeLayout(NSAttributedString(string: self.placeholder, font: searchBarFont, textColor: self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth, height: fieldHeight), visibleProgress, self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93), self.theme?.rootController.navigationSearchBar.inputFillColor ?? .clear, self.theme?.rootController.navigationBar.opaqueBackgroundColor ?? .clear, transition) + + let placeholderString = NSAttributedString(string: self.placeholder, font: searchBarFont, textColor: self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)) + let compactPlaceholderString = NSAttributedString(string: self.compactPlaceholder, font: searchBarFont, textColor: self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)) + + let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93), self.theme?.rootController.navigationSearchBar.inputFillColor ?? .clear, self.theme?.rootController.navigationBar.opaqueBackgroundColor ?? .clear, transition) searchBarApply() let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: 8.0 + overscrollProgress * fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight)) diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index ed4dc6974f..d00ff9803b 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -156,7 +156,7 @@ public final class SearchDisplayController { self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: safeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) } - public func activate(insertSubnode: @escaping (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?) { + public func activate(insertSubnode: @escaping (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?, focus: Bool = true) { guard let (layout, navigationBarHeight) = self.containerLayout else { return } @@ -231,7 +231,9 @@ public final class SearchDisplayController { insertSubnode(self.searchBar, true) self.searchBar.layout() - self.searchBar.activate() + if focus { + self.searchBar.activate() + } if let placeholder = placeholder { self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) if self.contentNode.hasDim { diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationSearchItem.swift b/submodules/SettingsUI/Sources/Notifications/NotificationSearchItem.swift index 7276c18065..49fd2c6d87 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationSearchItem.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationSearchItem.swift @@ -112,7 +112,8 @@ class NotificationSearchItemNode: ListViewItemNode { let backgroundColor = item.theme.chatList.itemBackgroundColor - let (_, searchBarApply) = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate) + let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)) + let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets()) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift index f8a57864bc..9a6291edf4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift @@ -190,6 +190,13 @@ public func addRecentDownloadItem(postbox: Postbox, item: RecentDownloadItem) -> |> ignoreValues } +public func clearRecentDownloadList(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads, items: []) + } + |> ignoreValues +} + public func markRecentDownloadItemsAsSeen(postbox: Postbox, items: [(messageId: MessageId, resourceId: String)]) -> Signal { return postbox.transaction { transaction -> Void in var unseenIds: [(messageId: MessageId, resourceId: String)] = [] diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 85bd993b9e..771bf69fa1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -20,7 +20,7 @@ import ContextUI import ChatPresentationInterfaceState private struct FetchControls { - let fetch: () -> Void + let fetch: (Bool) -> Void let cancel: () -> Void } @@ -228,7 +228,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } case .Remote, .Paused: if let fetch = self.fetchControls.with({ return $0?.fetch }) { - fetch() + fetch(true) } case .Local: break @@ -251,7 +251,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } case .Remote, .Paused: if let fetch = self.fetchControls.with({ return $0?.fetch }) { - fetch() + fetch(true) } case .Local: self.activateLocalContent() @@ -316,9 +316,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { updateImageSignal = chatMessageImageFile(account: arguments.context.account, fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } - updatedFetchControls = FetchControls(fetch: { [weak self] in + updatedFetchControls = FetchControls(fetch: { [weak self] userInitiated in if let strongSelf = self { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: arguments.context, message: arguments.message, file: arguments.file, userInitiated: true).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: arguments.context, message: arguments.message, file: arguments.file, userInitiated: userInitiated).start()) } }, cancel: { messageMediaFileCancelInteractiveFetch(context: arguments.context, messageId: arguments.message.id, file: arguments.file) @@ -331,15 +331,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { |> map { resourceStatus, actualFetchStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, actualFetchStatus) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) } else { updatedStatusSignal = messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions) |> map { resourceStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) } - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) } var consumableContentIcon: UIImage? @@ -765,7 +765,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if arguments.automaticDownload { - updatedFetchControls.fetch() + updatedFetchControls.fetch(false) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index e447871341..3a3746ed56 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -720,7 +720,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5) - let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: item.message, isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false) + let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: item.message, isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false, isDownloadList: false) playbackStatusNode.status = status self.durationNode?.status = status |> map(Optional.init)