From 8086ea1ecb5c6cda95116d2884f54dcbd12ea99a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 22 Feb 2022 22:08:42 +0400 Subject: [PATCH 1/4] 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) From 01709f6e5527448ca01dc8179a7f6eb3b4c99d2b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 22 Feb 2022 22:08:52 +0400 Subject: [PATCH 2/4] Support updated tgcalls --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../Sources/PresentationGroupCall.swift | 44 ++++++++++++++++--- .../TelegramEngine/Calls/GroupCalls.swift | 7 +++ .../Sources/GroupCallContext.swift | 15 +++++-- .../OngoingCallThreadLocalContext.h | 2 +- .../Sources/OngoingCallThreadLocalContext.mm | 4 +- submodules/TgVoipWebrtc/tgcalls | 2 +- 7 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 32f7a84b03..8b26f37f09 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -443,6 +443,7 @@ "DialogList.TabTitle" = "Chats"; "DialogList.Title" = "Chats"; "DialogList.SearchLabel" = "Search for messages or users"; +"DialogList.SearchLabelCompact" = "Search"; "DialogList.NoMessagesTitle" = "You have no conversations yet"; "DialogList.NoMessagesText" = "Start messaging by pressing the pencil button in the top right corner or go to the Contacts section."; "DialogList.SingleTypingSuffix" = "%@ is typing"; diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 529ba8e2db..523f7f1151 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1511,12 +1511,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { switch joinCallResult.connectionMode { case .rtc: strongSelf.currentConnectionMode = .rtc - strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false) strongSelf.genericCallContext?.setJoinResponse(payload: clientParams) case let .broadcast(isExternalStream): strongSelf.currentConnectionMode = .broadcast strongSelf.genericCallContext?.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: isExternalStream)) - strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false) + strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: isExternalStream) } strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl) @@ -1784,15 +1784,25 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + let chatPeer = self.accountContext.account.postbox.peerView(id: self.peerId) + |> map { view -> Peer? in + if let peer = peerViewMainPeer(view) { + return peer + } else { + return nil + } + } + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), participantsContext.state, participantsContext.activeSpeakers, self.speakingParticipantsContext.get(), adminIds, myPeer, + chatPeer, accountContext.account.postbox.peerView(id: peerId), self.isReconnectingAsSpeakerPromise.get() - ).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds, myPeerAndCachedData, view, isReconnectingAsSpeaker in + ).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds, myPeerAndCachedData, chatPeer, view, isReconnectingAsSpeaker in guard let strongSelf = self else { return } @@ -1874,6 +1884,30 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) } } + + if let chatPeer = chatPeer, !participants.contains(where: { $0.peer.id == chatPeer.id }) { + participants.append(GroupCallParticipantsContext.Participant( + peer: chatPeer, + ssrc: 100, + videoDescription: GroupCallParticipantsContext.Participant.VideoDescription( + endpointId: "unified", + ssrcGroups: [], + audioSsrc: 100, + isPaused: false + ), + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: nil, + hasRaiseHand: false, + activityTimestamp: nil, + activityRank: nil, + muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false), + volume: nil, + about: nil, + joinedVideo: false + )) + participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) + } var otherParticipantsWithVideo = 0 var videoWatchingParticipants = 0 @@ -2691,7 +2725,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } let clientParams = joinCallResult.jsonParams - screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false) screencastCallContext.setJoinResponse(payload: clientParams) }, error: { error in guard let _ = self else { @@ -2885,7 +2919,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if !self.didInitializeConnectionMode || self.currentConnectionMode != .none { self.didInitializeConnectionMode = true self.currentConnectionMode = .none - self.genericCallContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc) + self.genericCallContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false) } self.internalState = .requesting diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index c0405f0ddc..8901dc3beb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -863,6 +863,13 @@ public final class GroupCallParticipantsContext { public var ssrcGroups: [SsrcGroup] public var audioSsrc: UInt32? public var isPaused: Bool + + public init(endpointId: String, ssrcGroups: [SsrcGroup], audioSsrc: UInt32?, isPaused: Bool) { + self.endpointId = endpointId + self.ssrcGroups = ssrcGroups + self.audioSsrc = audioSsrc + self.isPaused = isPaused + } } public var peer: Peer diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 2c6b3ca8b6..85fadf42b0 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -58,6 +58,8 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource { return self.engine.calls.requestStreamState(callId: self.callId, accessHash: self.accessHash).start(next: { result in if let channel = result?.channels.first { completion(channel.latestTimestamp) + } else { + completion(0) } }) } else { @@ -124,6 +126,11 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource { let part: OngoingGroupCallBroadcastPart switch result.status { case let .data(dataValue): + /*#if DEBUG + let tempFile = EngineTempBox.shared.tempFile(fileName: "part.mp4") + let _ = try? dataValue.write(to: URL(fileURLWithPath: tempFile.path)) + print("Dump stream part: \(tempFile.path)") + #endif*/ part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .success, oggData: dataValue) case .notReady: part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .notReady, oggData: Data()) @@ -606,7 +613,7 @@ public final class OngoingGroupCallContext { self.context.stop() } - func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool) { + func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool, isUnifiedBroadcast: Bool) { let mappedConnectionMode: OngoingCallConnectionMode switch connectionMode { case .none: @@ -616,7 +623,7 @@ public final class OngoingGroupCallContext { case .broadcast: mappedConnectionMode = .broadcast } - self.context.setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled) + self.context.setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled, isUnifiedBroadcast: isUnifiedBroadcast) if (mappedConnectionMode != .rtc) { self.joinPayload.set(.never()) @@ -900,9 +907,9 @@ public final class OngoingGroupCallContext { }) } - public func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool) { + public func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool, isUnifiedBroadcast: Bool) { self.impl.with { impl in - impl.setConnectionMode(connectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled) + impl.setConnectionMode(connectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled, isUnifiedBroadcast: isUnifiedBroadcast) } } diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 04ccfc4300..3e777135af 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -354,7 +354,7 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { - (void)stop; -- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled; +- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled isUnifiedBroadcast:(bool)isUnifiedBroadcast; - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; - (void)setJoinResponsePayload:(NSString * _Nonnull)payload; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index eccbff839f..11a638ddc4 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1580,7 +1580,7 @@ private: } } -- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled { +- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled isUnifiedBroadcast:(bool)isUnifiedBroadcast { if (_instance) { tgcalls::GroupConnectionMode mappedConnectionMode; switch (connectionMode) { @@ -1601,7 +1601,7 @@ private: break; } } - _instance->setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled); + _instance->setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled, isUnifiedBroadcast); } } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 20860ca291..382d1b6756 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 20860ca29147b4faa4f0b75a0da58517d9d4856c +Subproject commit 382d1b6756768021274cb1edfc1e144cfb101fb8 From 4fea040b928c8d664d0e5dc6ce7986723d68e99b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 22 Feb 2022 22:21:37 +0400 Subject: [PATCH 3/4] Remove alignment --- .../Sources/AnimatedStickerFrameSource.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift index 3d62dec40b..6fb54ca3e6 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift @@ -280,8 +280,8 @@ private final class AnimatedStickerDirectFrameSourceCache { self.storeQueue = sharedStoreQueue self.frameCount = frameCount - self.width = alignUp(size: width, align: 8) - self.height = alignUp(size: height, align: 8) + self.width = width// alignUp(size: width, align: 8) + self.height = height//alignUp(size: height, align: 8) self.useHardware = useHardware let suffix : String From 2c3a1c802be293d806c9d3c805370cd515ff86c4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 22 Feb 2022 23:49:26 +0300 Subject: [PATCH 4/4] Attachment menu improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 21 ++ .../Sources/AttachmentController.swift | 5 + .../Sources/AttachmentPanel.swift | 2 +- submodules/Display/Source/NavigationBar.swift | 14 ++ .../LegacyComponents/Sources/PGCamera.m | 3 + .../Sources/LocationPickerController.swift | 2 +- .../Sources/MediaPickerGridItem.swift | 8 +- .../Sources/MediaPickerManageNode.swift | 2 +- .../Sources/MediaPickerScreen.swift | 180 +++++++++++------- .../Sources/MediaPickerSelectedListNode.swift | 46 ++++- .../Sources/AttachmentFileController.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 36 +++- .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 12 +- 14 files changed, 249 insertions(+), 85 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a8b5dfd7f8..b66153ba30 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7304,3 +7304,24 @@ Sorry for the inconvenience."; "Attachment.OpenSettings" = "Go to Settings"; "Attachment.OpenCamera" = "Open Camera"; + +"Attachment.DeselectedPhotos_1" = "%@ photo deselected"; +"Attachment.DeselectedPhotos_2" = "%@ photos deselected"; +"Attachment.DeselectedPhotos_3_10" = "%@ photos deselected"; +"Attachment.DeselectedPhotos_any" = "%@ photos deselected"; +"Attachment.DeselectedPhotos_many" = "%@ photos deselected"; +"Attachment.DeselectedPhotos_0" = "%@ photos deselected"; + +"Attachment.DeselectedVideos_1" = "%@ video deselected"; +"Attachment.DeselectedVideos_2" = "%@ videos deselected"; +"Attachment.DeselectedVideos_3_10" = "%@ videos deselected"; +"Attachment.DeselectedVideos_any" = "%@ videos deselected"; +"Attachment.DeselectedVideos_many" = "%@ videos deselected"; +"Attachment.DeselectedVideos_0" = "%@ videos deselected"; + +"Attachment.DeselectedItems_1" = "%@ item deselected"; +"Attachment.DeselectedItems_2" = "%@ items deselected"; +"Attachment.DeselectedItems_3_10" = "%@ items deselected"; +"Attachment.DeselectedItems_any" = "%@ items deselected"; +"Attachment.DeselectedItems_many" = "%@ items deselected"; +"Attachment.DeselectedItems_0" = "%@ items deselected"; diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 572ddcefaa..95c5d74c56 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -23,10 +23,15 @@ public enum AttachmentButtonType: Equatable { public protocol AttachmentContainable: ViewController { var requestAttachmentMenuExpansion: () -> Void { get set } + func resetForReuse() func prepareForReuse() } public extension AttachmentContainable { + func resetForReuse() { + + } + func prepareForReuse() { } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index aa086f06e7..cd6f535a4f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -110,7 +110,7 @@ private final class AttachButtonComponent: CombinedComponent { availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) - + let title = title.update( component: Text( text: name, diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index ecc1dde49c..1ab09e7555 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1106,6 +1106,11 @@ open class NavigationBar: ASDisplayNode { let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) leftTitleInset = leftButtonSize.width + leftButtonInset + 1.0 + var transition = transition + if self.leftButtonNode.frame.width.isZero { + transition = .immediate + } + self.leftButtonNode.alpha = 1.0 transition.updateFrame(node: self.leftButtonNode, frame: CGRect(origin: CGPoint(x: leftButtonInset, y: contentVerticalOrigin + floor((nominalHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize)) } @@ -1118,6 +1123,11 @@ open class NavigationBar: ASDisplayNode { let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape) rightTitleInset = rightButtonSize.width + leftButtonInset + 1.0 self.rightButtonNode.alpha = 1.0 + + var transition = transition + if self.rightButtonNode.frame.width.isZero { + transition = .immediate + } transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize)) } @@ -1182,6 +1192,10 @@ open class NavigationBar: ASDisplayNode { self.titleNode.alpha = progress * progress } } else { + var transition = transition + if self.titleNode.frame.width.isZero { + transition = .immediate + } self.titleNode.alpha = 1.0 transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)) } diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index 4ffeb5f002..84c5399a6d 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -531,6 +531,9 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus"; { TGDispatchOnMainThread(^ { + if (!_subscribedForCameraChanges) { + return; + } if ([keyPath isEqualToString:PGCameraAdjustingFocusKey]) { bool adjustingFocus = [[change objectForKey:NSKeyValueChangeNewKey] isEqualToNumber:@YES]; diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index a48f81cf9d..c6429d4081 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -337,7 +337,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab self.interaction?.openSearch() } - public func prepareForReuse() { + public func resetForReuse() { self.interaction?.updateMapMode(.map) self.interaction?.dismissSearch() self.scrollToTop?() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 9126d30cf2..9f5d063a3b 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -127,7 +127,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.checkNode = checkNode self.setNeedsLayout() } - + if let asset = self.asset, let interaction = self.interaction, let selectionState = interaction.selectionState { let selected = selectionState.isIdentifierSelected(asset.localIdentifier) if let legacyAsset = TGMediaAsset(phAsset: asset) { @@ -177,7 +177,7 @@ final class MediaPickerGridItemNode: GridItemNode { } }, error: { _ in }, completed: nil)! - + return ActionDisposable { disposable.dispose() } @@ -185,9 +185,9 @@ final class MediaPickerGridItemNode: GridItemNode { return EmptyDisposable } } - + let scale = min(2.0, UIScreenScale) - let targetSize = CGSize(width: 140.0 * scale, height: 140.0 * scale) + let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) let originalSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false) let imageSignal: Signal = editedSignal |> mapToSignal { result in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift index 2be6981d24..c48fc25276 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift @@ -58,7 +58,7 @@ final class MediaPickerManageNode: ASDisplayNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .left) let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0 - buttonWidth - 26.0, height: layout.size.height)) - let panelHeight = max(64.0, textSize.height + 10.0) + let panelHeight = max(64.0, textSize.height + 24.0) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: floorToScreenPixels((panelHeight - textSize.height) / 2.0) - 5.0), size: textSize)) if themeUpdated { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index a96d50b22d..ea1c2abd14 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -8,6 +8,7 @@ import SwiftSignalKit import AccountContext import TelegramPresentationData import TelegramUIPreferences +import TelegramStringFormatting import MergeLists import Photos import PhotosUI @@ -75,8 +76,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + fileprivate var interaction: MediaPickerInteraction? + private let peer: EnginePeer? private let chatLocation: ChatLocation? + private let bannedSendMedia: (Int32, Bool)? private let titleView: MediaPickerTitleView private let moreButtonNode: MediaPickerMoreButtonNode @@ -102,19 +106,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { enum State { case noAccess(cameraAccess: AVAuthorizationStatus?) - case assets(fetchResult: PHFetchResult?, mediaAccess: PHAuthorizationStatus, cameraAccess: AVAuthorizationStatus?) + case assets(fetchResult: PHFetchResult?, preload: Bool, mediaAccess: PHAuthorizationStatus, cameraAccess: AVAuthorizationStatus?) } private weak var controller: MediaPickerScreen? - fileprivate var interaction: MediaPickerInteraction? private var presentationData: PresentationData private let mediaAssetsContext: MediaAssetsContext private let backgroundNode: NavigationBackgroundNode private let gridNode: GridNode - private var cameraView: TGAttachmentCameraView? + fileprivate var cameraView: TGAttachmentCameraView? private var placeholderNode: MediaPickerPlaceholderNode? private var manageNode: MediaPickerManageNode? + private var bannedTextNode: ImmediateTextNode? private var selectionNode: MediaPickerSelectedListNode? @@ -123,6 +127,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var enqueuedTransactions: [MediaPickerGridTransaction] = [] private var state: State? + private var preloadPromise = ValuePromise(true) + private var itemsDisposable: Disposable? private var selectionChangedDisposable: Disposable? private var itemsDimensionsUpdatedDisposable: Disposable? @@ -131,7 +137,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private let hiddenMediaId = Promise(nil) private var didSetReady = false - private let _ready = Promise() + private let _ready = Promise(true) var ready: Promise { return self._ready } @@ -154,45 +160,18 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.addSubnode(self.backgroundNode) self.addSubnode(self.gridNode) - - self.interaction = MediaPickerInteraction(openMedia: { [weak self] fetchResult, index, immediateThumbnail in - self?.openMedia(fetchResult: fetchResult, index: index, immediateThumbnail: immediateThumbnail) - }, openSelectedMedia: { [weak self] item, immediateThumbnail in - self?.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail) - }, toggleSelection: { [weak self] item, value in - if let strongSelf = self { - strongSelf.interaction?.selectionState?.setItem(item, selected: value) - } - }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in - if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState { - if let currentItem = currentItem { - selectionState.setItem(currentItem, selected: true) - } - strongSelf.send(silently: silently, scheduleTime: scheduleTime, animated: animated) - } - }, schedule: { [weak self] in - if let strongSelf = self { - strongSelf.controller?.presentSchedulePicker(false, { [weak self] time in - self?.interaction?.sendSelected(nil, false, time, true) - }) - } - }, dismissInput: { [weak self] in - if let strongSelf = self { - strongSelf.dismissInput() - } - }, selectionState: TGMediaSelectionContext(), editingState: TGMediaEditingContext()) - self.interaction?.selectionState?.grouping = true - + + let preloadPromise = self.preloadPromise let updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess()) |> mapToSignal { mediaAccess, cameraAccess -> Signal in if case .notDetermined = mediaAccess { - return .single(.assets(fetchResult: nil, mediaAccess: mediaAccess, cameraAccess: cameraAccess)) + return .single(.assets(fetchResult: nil, preload: false, mediaAccess: mediaAccess, cameraAccess: cameraAccess)) } else if [.restricted, .denied].contains(mediaAccess) { return .single(.noAccess(cameraAccess: cameraAccess)) } else { - return mediaAssetsContext.recentAssets() - |> map { fetchResult in - return .assets(fetchResult: fetchResult, mediaAccess: mediaAccess, cameraAccess: cameraAccess) + return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get()) + |> map { fetchResult, preload in + return .assets(fetchResult: fetchResult, preload: preload, mediaAccess: mediaAccess, cameraAccess: cameraAccess) } } } @@ -212,7 +191,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.hiddenMediaDisposable = (self.hiddenMediaId.get() |> deliverOnMainQueue).start(next: { [weak self] id in if let strongSelf = self { - strongSelf.interaction?.hiddenMediaId = id + strongSelf.controller?.interaction?.hiddenMediaId = id strongSelf.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { @@ -224,7 +203,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } }) - if let selectionState = self.interaction?.selectionState { + if let selectionState = self.controller?.interaction?.selectionState { func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal { return Signal { subscriber in let disposable = selectionState.selectionChangedSignal()?.start(next: { next in @@ -244,7 +223,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }) } - if let editingState = self.interaction?.editingState { + if let editingState = self.controller?.interaction?.editingState { func itemsDimensionsUpdatedSignal(editingState: TGMediaEditingContext) -> Signal { return Signal { subscriber in let disposable = editingState.cropAdjustmentsUpdatedSignal()?.start(next: { next in @@ -290,11 +269,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { cameraView.startPreview() self.gridNode.scrollView.addSubview(cameraView) - + // self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) } - private func dismissInput() { + fileprivate func dismissInput() { self.view.window?.endEditing(true) } @@ -302,7 +281,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var requestedCameraAccess = false private func updateState(_ state: State) { - guard let interaction = self.interaction, let controller = self.controller else { + guard let controller = self.controller, let interaction = controller.interaction else { return } @@ -325,14 +304,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.requestedCameraAccess = true self.mediaAssetsContext.requestCameraAccess() } - case let .assets(fetchResult, mediaAccess, cameraAccess): + case let .assets(fetchResult, preload, mediaAccess, cameraAccess): if let fetchResult = fetchResult { - for i in 0 ..< fetchResult.count { - entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, fetchResult.count - i - 1))) + let totalCount = fetchResult.count + let count = preload ? min(10, totalCount) : totalCount + + for i in 0 ..< count { + entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, totalCount - i - 1))) stableId += 1 } - if case let .assets(previousFetchResult, _, previousCameraAccess) = previousState, previousFetchResult == nil || previousCameraAccess != cameraAccess { + if case let .assets(previousFetchResult, _, _, previousCameraAccess) = previousState, previousFetchResult == nil || previousCameraAccess != cameraAccess { updateLayout = true } @@ -365,7 +347,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } self.selectionNode?.updateSelectionState() - let count = Int32(self.interaction?.selectionState?.count() ?? 0) + let count = Int32(self.controller?.interaction?.selectionState?.count() ?? 0) self.controller?.updateSelectionState(count: count) if let (layout, navigationBarHeight) = self.validLayout { @@ -387,7 +369,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let selectionNode = MediaPickerSelectedListNode(context: controller.context) selectionNode.alpha = 0.0 selectionNode.isUserInteractionEnabled = false - selectionNode.interaction = self.interaction + selectionNode.interaction = self.controller?.interaction self.insertSubnode(selectionNode, aboveSubnode: self.gridNode) self.selectionNode = selectionNode @@ -405,8 +387,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - private func openMedia(fetchResult: PHFetchResult, index: Int, immediateThumbnail: UIImage?) { - guard let controller = self.controller, let interaction = self.interaction, let (layout, _) = self.validLayout else { + fileprivate func openMedia(fetchResult: PHFetchResult, index: Int, immediateThumbnail: UIImage?) { + guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else { return } Queue.mainQueue().justDispatch { @@ -422,15 +404,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return self?.transitionView(for: identifier) }, completed: { [weak self] result, silently, scheduleTime in if let strongSelf = self { - strongSelf.interaction?.sendSelected(result, silently, scheduleTime, false) + strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false) } }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a) }) } - private func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) { - guard let controller = self.controller, let interaction = self.interaction, let (layout, _) = self.validLayout else { + fileprivate func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) { + guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else { return } Queue.mainQueue().justDispatch { @@ -445,7 +427,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return self?.transitionView(for: identifier) }, completed: { [weak self] result, silently, scheduleTime in if let strongSelf = self { - strongSelf.interaction?.sendSelected(result, silently, scheduleTime, false) + strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false) } }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a) @@ -453,7 +435,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool) { - guard let signals = TGMediaAssetsController.resultSignals(for: self.interaction?.selectionState, editingContext: self.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else { + guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else { return } self.controller?.legacyCompletion(signals, silently, scheduleTime) @@ -549,11 +531,42 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var cameraRect: CGRect? = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: itemWidth, height: itemWidth * 2.0 + 1.0)) var manageHeight: CGFloat = 0.0 - if case let .assets(_, mediaAccess, cameraAccess) = self.state { + if case let .assets(_, _, mediaAccess, cameraAccess) = self.state { if cameraAccess == nil { cameraRect = nil } - if case .notDetermined = mediaAccess { + if let (untilDate, personal) = self.controller?.bannedSendMedia { + self.gridNode.alpha = 0.3 + self.gridNode.isUserInteractionEnabled = false + + let banDescription: String + if untilDate != 0 && untilDate != Int32.max { + banDescription = self.presentationData.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)).string + } else if personal { + banDescription = self.presentationData.strings.Conversation_RestrictedMedia + } else { + banDescription = self.presentationData.strings.Conversation_DefaultRestrictedMedia + } + + let bannedTextNode: ImmediateTextNode + if let current = self.bannedTextNode { + bannedTextNode = current + } else { + bannedTextNode = ImmediateTextNode() + bannedTextNode.maximumNumberOfLines = 0 + bannedTextNode.textAlignment = .center + self.bannedTextNode = bannedTextNode + self.addSubnode(bannedTextNode) + } + + bannedTextNode.attributedText = NSAttributedString(string: banDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.list.freeTextColor, paragraphAlignment: .center) + + let bannedTextSize = bannedTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0, height: layout.size.height)) + + manageHeight = max(44.0, bannedTextSize.height + 20.0) + + transition.updateFrame(node: bannedTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - bannedTextSize.width) / 2.0), y: insets.top + floorToScreenPixels((manageHeight - bannedTextSize.height) / 2.0) - 4.0), size: bannedTextSize)) + } else if case .notDetermined = mediaAccess { } else { if case .limited = mediaAccess { @@ -604,7 +617,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { transition.updateFrame(node: self.backgroundNode, frame: bounds) self.backgroundNode.update(size: bounds.size, transition: transition) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -612,12 +625,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.didSetReady = true Queue.mainQueue().justDispatch { strongSelf._ready.set(.single(true)) + + Queue.mainQueue().after(0.5, { + strongSelf.preloadPromise.set(false) + }) } } }) if let selectionNode = self.selectionNode { - let selectedItems = self.interaction?.selectionState?.selectedItems() as? [TGMediaSelectableItem] ?? [] + let selectedItems = self.controller?.interaction?.selectionState?.selectedItems() as? [TGMediaSelectableItem] ?? [] let updateSelectionNode = { selectionNode.updateLayout(size: bounds.size, insets: cleanGridInsets, items: selectedItems, grouped: self.controller?.groupedValue ?? true, theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, bubbleCorners: self.presentationData.chatBubbleCorners, transition: transition) } @@ -690,7 +707,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var groupedValue: Bool = true { didSet { self.groupedPromise.set(self.groupedValue) - self.controllerNode.interaction?.selectionState?.grouping = self.groupedValue + self.interaction?.selectionState?.grouping = self.groupedValue if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .immediate) @@ -699,11 +716,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } private let groupedPromise = ValuePromise(true) - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.peer = peer self.chatLocation = chatLocation + self.bannedSendMedia = bannedSendMedia self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0) self.titleView.title = self.presentationData.strings.Attachment_Gallery @@ -763,6 +781,34 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } } + + self.interaction = MediaPickerInteraction(openMedia: { [weak self] fetchResult, index, immediateThumbnail in + self?.controllerNode.openMedia(fetchResult: fetchResult, index: index, immediateThumbnail: immediateThumbnail) + }, openSelectedMedia: { [weak self] item, immediateThumbnail in + self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail) + }, toggleSelection: { [weak self] item, value in + if let strongSelf = self { + strongSelf.interaction?.selectionState?.setItem(item, selected: value) + } + }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in + if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState { + if let currentItem = currentItem { + selectionState.setItem(currentItem, selected: true) + } + strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated) + } + }, schedule: { [weak self] in + if let strongSelf = self { + strongSelf.presentSchedulePicker(false, { [weak self] time in + self?.interaction?.sendSelected(nil, false, time, true) + }) + } + }, dismissInput: { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.dismissInput() + } + }, selectionState: TGMediaSelectionContext(), editingState: TGMediaEditingContext()) + self.interaction?.selectionState?.grouping = true } required init(coder aDecoder: NSCoder) { @@ -811,12 +857,18 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.dismiss() } - public func prepareForReuse() { + public func resetForReuse() { if let webSearchController = self.webSearchController { self.webSearchController = nil webSearchController.dismiss() } self.scrollToTop?() + + self.controllerNode.cameraView?.pausePreview() + } + + public func prepareForReuse() { + self.controllerNode.cameraView?.resumePreview() } @objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { @@ -881,7 +933,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } public var mediaPickerContext: AttachmentMediaPickerContext? { - if let interaction = self.controllerNode.interaction { + if let interaction = self.interaction { return MediaPickerContext(interaction: interaction) } else { return nil diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index d9455ed552..ecf456e9d8 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -189,7 +189,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } } -final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate { +final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let context: AccountContext fileprivate let wallpaperBackgroundNode: WallpaperBackgroundNode @@ -226,9 +226,11 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate { } self.scrollNode.view.delegate = self + self.scrollNode.view.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(ReorderingGestureRecognizer(shouldBegin: { [weak self] point in - if let strongSelf = self, !strongSelf.scrollNode.view.isTracking { + if let strongSelf = self, !strongSelf.scrollNode.view.isDragging { + let point = strongSelf.view.convert(point, to: strongSelf.scrollNode.view) for (_, itemNode) in strongSelf.itemNodes { if itemNode.frame.contains(point) { return (true, true, itemNode) @@ -242,20 +244,54 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate { }, began: { [weak self] itemNode in self?.beginReordering(itemNode: itemNode) }, ended: { [weak self] point in - self?.endReordering(point: point) + if let strongSelf = self { + if var point = point { + point = strongSelf.view.convert(point, to: strongSelf.scrollNode.view) + strongSelf.endReordering(point: point) + } else { + strongSelf.endReordering(point: nil) + } + } }, moved: { [weak self] offset in self?.updateReordering(offset: offset) })) + + Queue.mainQueue().after(0.1, { + self.updateAbsoluteRects() + }) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.interaction?.dismissInput() } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateAbsoluteRects() + } + func scrollToTop(animated: Bool) { self.scrollNode.view.setContentOffset(CGPoint(), animated: animated) } + func updateAbsoluteRects() { + guard let messageNodes = self.messageNodes, let (size, _, _, _, _, _, _) = self.validLayout else { + return + } + + for itemNode in messageNodes { + var absoluteRect = itemNode.frame + if let supernode = self.supernode { + absoluteRect = supernode.convert(itemNode.bounds, from: itemNode) + } + absoluteRect.origin.y = size.height - absoluteRect.origin.y - absoluteRect.size.height + itemNode.updateAbsoluteRect(absoluteRect, within: self.bounds.size) + } + } + private func beginReordering(itemNode: MediaPickerSelectedItemNode) { self.isReordering = true @@ -265,7 +301,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate { let reorderNode = ReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) self.reorderNode = reorderNode - self.addSubnode(reorderNode) + self.scrollNode.addSubnode(reorderNode) itemNode.isHidden = true @@ -549,6 +585,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate { } } + self.updateAbsoluteRects() + self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight) } diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index d2175725df..c458d54394 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -164,7 +164,7 @@ private class AttachmentFileControllerImpl: ItemListController, AttachmentContai public var requestAttachmentMenuExpansion: () -> Void = {} var prepareForReuseImpl: () -> Void = {} - public func prepareForReuse() { + public func resetForReuse() { self.prepareForReuseImpl() self.scrollToTop?() } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e9328530cc..54e29ef7b6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9121,6 +9121,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var layout = layout + if let _ = self.attachmentController { + layout = layout.withUpdatedInputHeight(nil) + } + var navigationBarTransition = transition self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) @@ -10320,18 +10325,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.chatDisplayNode.dismissInput() - - let currentFilesController = Atomic(value: nil) - let currentLocationController = Atomic(value: nil) - + + var bannedSendMedia: (Int32, Bool)? var canSendPolls = true if peer is TelegramUser || peer is TelegramSecretChat { canSendPolls = false } else if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendMedia) { + bannedSendMedia = value + } if channel.hasBannedPermission(.banSendPolls) != nil { canSendPolls = false } } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendMedia) { + bannedSendMedia = (Int32.max, false) + } if group.hasBannedPermission(.banSendPolls) { canSendPolls = false } @@ -10344,6 +10353,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText + let currentMediaController = Atomic(value: nil) + let currentFilesController = Atomic(value: nil) + let currentLocationController = Atomic(value: nil) + let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, buttons: availableTabs) attachmentController.requestController = { [weak self, weak attachmentController] type, completion in guard let strongSelf = self else { @@ -10351,7 +10364,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } switch type { case .gallery: - strongSelf.presentMediaPicker(present: { controller, mediaPickerContext in + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentMediaController.with { $0 } + if let controller = existingController { + controller.prepareForReuse() + completion(controller, nil) + return + } + strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in + let _ = currentMediaController.swap(controller) completion(controller, mediaPickerContext) }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in attachmentController?.mediaPickerContext = mediaPickerContext @@ -10361,7 +10382,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) }) - strongSelf.controllerNavigationDisposable.set(nil) case .file: strongSelf.controllerNavigationDisposable.set(nil) let existingController = currentFilesController.with { $0 } @@ -10978,11 +10998,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(actionSheet, in: .window(.root)) } - private func presentMediaPicker(present: @escaping (AttachmentContainable, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) { + private func presentMediaPicker(bannedSendMedia: (Int32, Bool)?, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } - let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), chatLocation: self.chatLocation) + let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), chatLocation: self.chatLocation, bannedSendMedia: bannedSendMedia) let mediaPickerContext = controller.mediaPickerContext controller.openCamera = { [weak self] cameraView in self?.openCamera(cameraView: cameraView) diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 6b3a4b2abd..4ecfd52eec 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -38,6 +38,7 @@ public enum UndoOverlayContent { case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) case inviteRequestSent(title: String, text: String) + case image(image: UIImage, text: String) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 843cd8e24c..696404e3fc 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -749,6 +749,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .image(image, text): + self.avatarNode = nil + self.iconNode = ASImageNode() + self.iconNode?.image = image + self.iconCheckNode = nil + self.animationNode = nil + self.animatedStickerNode = nil + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + displayUndo = true + self.originalRemainingSeconds = 5 } self.remainingSeconds = self.originalRemainingSeconds @@ -777,7 +787,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved, .paymentSent, .inviteRequestSent: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent: break case .dice: self.panelWrapperNode.clipsToBounds = true