diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7d8afcf436..0f7cfdf475 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -351,6 +351,7 @@ public final class NavigateToChatControllerParams { public let activateInput: Bool public let keepStack: NavigateToChatKeepStack public let useExisting: Bool + public let useBackAnimation: Bool public let purposefulAction: (() -> Void)? public let scrollToEndIfExists: Bool public let activateMessageSearch: (ChatSearchDomain, String)? @@ -366,7 +367,7 @@ public final class NavigateToChatControllerParams { public let setupController: (ChatController) -> Void public let completion: (ChatController) -> Void - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -379,6 +380,7 @@ public final class NavigateToChatControllerParams { self.activateInput = activateInput self.keepStack = keepStack self.useExisting = useExisting + self.useBackAnimation = useBackAnimation self.purposefulAction = purposefulAction self.scrollToEndIfExists = scrollToEndIfExists self.activateMessageSearch = activateMessageSearch diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 7b159d6b19..38bbe6315c 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -160,7 +160,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { var imageApply: (() -> Void)? var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let photo = item.invoice.photo, let dimensions = photo.dimensions { - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimensions.cgSize.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: dimensions.cgSize.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) imageApply = makeImageLayout(arguments) maxTextWidth = max(1.0, maxTextWidth - imageSize.width - imageTextSpacing) if imageUpdated { diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 7a969b462a..b1d69b16eb 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -352,7 +352,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> take(1) - + self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer) |> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in if searchQuery?.isEmpty ?? true { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 153e602200..5707fcdd24 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -275,6 +275,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case recentlyDownloaded } + case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int, isFirstInList: Bool)?, MessageSection, Bool) @@ -282,126 +283,219 @@ public enum ChatListSearchEntry: Comparable, Identifiable { public var stableId: ChatListSearchEntryStableId { switch self { - case let .localPeer(peer, _, _, _, _, _, _, _, _): - return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _): - return .globalPeerId(peer.peer.id) - case let .message(message, _, _, _, _, _, _, _, _, section, _): - return .messageId(message.id, section) - case .addContact: - return .addContact + case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _): + return .localPeerId(peer.id) + case let .localPeer(peer, _, _, _, _, _, _, _, _): + return .localPeerId(peer.id) + case let .globalPeer(peer, _, _, _, _, _, _, _): + return .globalPeerId(peer.peer.id) + case let .message(message, _, _, _, _, _, _, _, _, section, _): + return .messageId(message.id, section) + case .addContact: + return .addContact } } public static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): - if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { - return true - } else { + case let .recentlySearchedPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder): + if case let .recentlySearchedPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 { + return true + } else { + return false + } + case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): + if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { + return true + } else { + return false + } + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { + return true + } else { + return false + } + case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused): + if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs { + if lhsMessage.id != rhsMessage.id { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { - return true - } else { + if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } - case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused): - if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs { - if lhsMessage.id != rhsMessage.id { - return false - } - if lhsMessage.stableVersion != rhsMessage.stableVersion { - return false - } - if lhsPeer != rhsPeer { - return false - } - if lhsPresentationData !== rhsPresentationData { - return false - } - if lhsCombinedPeerReadState != rhsCombinedPeerReadState { - return false - } - if lhsTotalCount != rhsTotalCount { - return false - } - if lhsSelected != rhsSelected { - return false - } - if lhsDisplayCustomHeader != rhsDisplayCustomHeader { - return false - } - if lhsKey != rhsKey { - return false - } - if lhsResourceId?.0 != rhsResourceId?.0 { - return false - } - if lhsResourceId?.1 != rhsResourceId?.1 { - return false - } - if lhsSection != rhsSection { - return false - } - if lhsAllPaused != rhsAllPaused { - return false - } - return true - } else { + if lhsPeer != rhsPeer { return false } - case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings): - if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs { - if lhsPhoneNumber != rhsPhoneNumber { - return false - } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - return true - } else { + if lhsPresentationData !== rhsPresentationData { return false } + if lhsCombinedPeerReadState != rhsCombinedPeerReadState { + return false + } + if lhsTotalCount != rhsTotalCount { + return false + } + if lhsSelected != rhsSelected { + return false + } + if lhsDisplayCustomHeader != rhsDisplayCustomHeader { + return false + } + if lhsKey != rhsKey { + return false + } + if lhsResourceId?.0 != rhsResourceId?.0 { + return false + } + if lhsResourceId?.1 != rhsResourceId?.1 { + return false + } + if lhsSection != rhsSection { + return false + } + if lhsAllPaused != rhsAllPaused { + return false + } + return true + } else { + return false + } + case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings): + if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs { + if lhsPhoneNumber != rhsPhoneNumber { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + } else { + return false + } } } public static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _): - if case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _) = rhs { - return lhsIndex <= rhsIndex - } else { - return true - } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _): - switch rhs { - case .localPeer: - return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _): - return lhsIndex <= rhsIndex - case .message, .addContact: - return true - } - case let .message(_, _, _, _, _, _, _, lhsKey, _, _, _): - if case let .message(_, _, _, _, _, _, _, rhsKey, _, _, _) = rhs { - return lhsKey < rhsKey - } else if case .addContact = rhs { - return true - } else { - return false - } - case .addContact: + case let .recentlySearchedPeer(_, _, _, lhsIndex, _, _, _, _): + if case let .recentlySearchedPeer(_, _, _, rhsIndex, _, _, _, _) = rhs { + return lhsIndex <= rhsIndex + } else { + return true + } + case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _): + switch rhs { + case .recentlySearchedPeer: return false + case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _): + return lhsIndex <= rhsIndex + case .globalPeer, .message, .addContact: + return true + } + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _): + switch rhs { + case .recentlySearchedPeer, .localPeer: + return false + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _): + return lhsIndex <= rhsIndex + case .message, .addContact: + return true + } + case let .message(_, _, _, _, _, _, _, lhsKey, _, _, _): + if case let .message(_, _, _, _, _, _, _, rhsKey, _, _, _) = rhs { + return lhsKey < rhsKey + } else if case .addContact = rhs { + return true + } else { + return false + } + case .addContact: + return false } } 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 .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder): + let primaryPeer: EnginePeer + var chatPeer: EnginePeer? + if let associatedPeer = associatedPeer { + primaryPeer = associatedPeer + chatPeer = peer + } else { + primaryPeer = peer + chatPeer = peer + } + + var enabled = true + if filter.contains(.onlyWriteable) { + if let peer = chatPeer { + enabled = canSendMessagesToPeer(peer._asPeer()) + } else { + enabled = false + } + } + if filter.contains(.onlyPrivateChats) { + if let peer = chatPeer { + switch peer { + case .user, .secretChat: + break + default: + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let peer = chatPeer { + if case .legacyGroup = peer { + } else if case let .channel(peer) = peer, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } + + var badge: ContactsPeerItemBadge? + if let unreadBadge = unreadBadge { + badge = ContactsPeerItemBadge(count: unreadBadge.0, type: unreadBadge.1 ? .inactive : .active) + } + + let header: ChatListSearchItemHeader? + if filter.contains(.removeSearchHeader) { + header = nil + } else { + let headerType: ChatListSearchItemHeaderType + if filter.contains(.onlyGroups) { + headerType = .chats + } else { + headerType = .recentPeers + } + header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: nil, action: nil) + } + + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in + if case let .peer(maybePeer, maybeChatPeer) = contactPeer, let peer = maybePeer, let chatPeer = maybeChatPeer { + interaction.peerSelected(chatPeer, peer, nil) + } else { + interaction.peerSelected(peer, nil, nil) + } + }, contextAction: peerContextAction.flatMap { peerContextAction in + return { node, gesture in + if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat { + peerContextAction(chatPeer, .search(nil), node, gesture) + } else { + gesture?.cancel() + } + } + }, arrowAction: nil) case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): let primaryPeer: EnginePeer var chatPeer: EnginePeer? @@ -1063,27 +1157,53 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) - let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError> + let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> if let query = query { - foundLocalPeers = context.engine.contacts.searchLocalPeers(query: query.lowercased()) - |> mapToSignal { local -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer]), NoError> in + foundLocalPeers = combineLatest( + context.engine.contacts.searchLocalPeers(query: query.lowercased()), + context.engine.peers.recentlySearchedPeers() + ) + |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set), NoError> in + let recentlySearched = allRecentlySearched.filter { peer in + guard let peer = peer.peer.peer else { + return false + } + return peer.indexName.matchesByTokens(query) + } + + var peerIds = Set() + + var peers: [EngineRenderedPeer] = [] + for peer in recentlySearched { + if !peerIds.contains(peer.peer.peerId) { + peerIds.insert(peer.peer.peerId) + peers.append(EngineRenderedPeer(peer.peer)) + } + } + for peer in local { + if !peerIds.contains(peer.peerId) { + peerIds.insert(peer.peerId) + peers.append(peer) + } + } + return context.engine.data.subscribe( EngineDataMap( - local.map { peer -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in - return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.peerId) + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) } ), EngineDataMap( - local.map { peer -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in - return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peer.peerId) + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) } ) ) |> map { notificationSettings, unreadCounts in - return (notificationSettings, unreadCounts, local) + return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId))) } } - |> map { notificationSettings, unreadCounts, peers -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]) in + |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in var unread: [EnginePeer.Id: (Int32, Bool)] = [:] for peer in peers { var isMuted: Bool = false @@ -1101,10 +1221,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { unread[peer.peerId] = (Int32(unreadCount), isMuted) } } - return (peers: peers, unread: unread) + return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) } } else { - foundLocalPeers = .single((peers: [], unread: [:])) + foundLocalPeers = .single((peers: [], unread: [:], recentlySearchedPeerIds: Set())) } let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> @@ -1293,10 +1413,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - var numberOfLocalPeers = 0 for renderedPeer in foundLocalPeers.peers { - if case .expand = localExpandType, numberOfLocalPeers >= 3 { - break + if !foundLocalPeers.recentlySearchedPeerIds.contains(renderedPeer.peerId) { + continue } if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, EnginePeer(accountPeer)) { @@ -1306,6 +1425,31 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case let .secretChat(secretChat) = peer, let associatedPeerId = secretChat.associatedPeerId { associatedPeer = renderedPeer.peers[associatedPeerId] } + + entries.append(.recentlySearchedPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + + index += 1 + } + } + } + + var numberOfLocalPeers = 0 + for renderedPeer in foundLocalPeers.peers { + if case .expand = localExpandType, numberOfLocalPeers >= 3 { + break + } + if foundLocalPeers.recentlySearchedPeerIds.contains(renderedPeer.peerId) { + continue + } + + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, EnginePeer(accountPeer)) { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + var associatedPeer: EnginePeer? + if case let .secretChat(secretChat) = peer, let associatedPeerId = secretChat.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) index += 1 numberOfLocalPeers += 1 diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 059c716c1a..b7a9f8bd23 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -1274,6 +1274,12 @@ open class NavigationController: UINavigationController, ContainableController, completion() } + public func replaceControllers(controllers: [UIViewController], animated: Bool, options: NavigationAnimationOptions = [], ready: ValuePromise? = nil, completion: @escaping () -> Void = {}) { + ready?.set(true) + self.setViewControllers(controllers, animated: animated) + completion() + } + public func replaceAllButRootController(_ controller: ViewController, animated: Bool, animationOptions: NavigationAnimationOptions = [], ready: ValuePromise? = nil, completion: @escaping () -> Void = {}) { ready?.set(true) var controllers = self.viewControllers diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 9d5681fa61..7ae5d57e08 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -110,9 +110,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.contentContainer.addSubnode(self.scrollNode) self.contentContainerMask = UIImageView() - let maskGradientWidth: CGFloat = 10.0 - self.contentContainerMask.image = generateImage(CGSize(width: maskGradientWidth * 2.0 + 1.0, height: 8.0), rotatedContext: { size, context in + self.contentContainerMask.image = generateImage(CGSize(width: 52.0, height: 52.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: 1.1) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) let shadowColor = UIColor.black @@ -122,17 +124,21 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { for i in 0 ... stepCount { let t = CGFloat(i) / CGFloat(stepCount) - colors.append(shadowColor.withAlphaComponent(t * t).cgColor) + colors.append(shadowColor.withAlphaComponent(t).cgColor) locations.append(t) } let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: maskGradientWidth, y: 0.0), options: CGGradientDrawingOptions()) - context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: maskGradientWidth + 1.0, y: 0.0), options: CGGradientDrawingOptions()) + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let gradientWidth = 6.0 + context.drawRadialGradient(gradient, startCenter: center, startRadius: size.width / 2.0, endCenter: center, endRadius: size.width / 2.0 - gradientWidth, options: []) + context.setFillColor(shadowColor.cgColor) - context.fill(CGRect(origin: CGPoint(x: maskGradientWidth, y: 0.0), size: CGSize(width: 1.0, height: size.height))) - })?.stretchableImage(withLeftCapWidth: Int(maskGradientWidth), topCapHeight: 0) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: gradientWidth - 1.0, dy: gradientWidth - 1.0)) + })?.stretchableImage(withLeftCapWidth: Int(52.0 / 2.0), topCapHeight: Int(52.0 / 2.0)) self.contentContainer.view.mask = self.contentContainerMask + //self.contentContainer.view.addSubview(self.contentContainerMask) super.init() diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 754824839f..b0c40c955d 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -567,6 +567,8 @@ public final class MediaStreamComponent: CombinedComponent { private var scheduledDismissUITimer: SwiftSignalKit.Timer? + let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self) + init(call: PresentationGroupCallImpl) { self.call = call @@ -639,8 +641,6 @@ public final class MediaStreamComponent: CombinedComponent { } }) - //let _ = call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false).start() - self.isVisibleInHierarchyDisposable = (call.accountContext.sharedContext.applicationBindings.applicationInForeground |> deliverOnMainQueue).start(next: { [weak self] inForeground in guard let strongSelf = self else { @@ -649,6 +649,16 @@ public final class MediaStreamComponent: CombinedComponent { if strongSelf.isVisibleInHierarchy != inForeground { strongSelf.isVisibleInHierarchy = inForeground strongSelf.updated(transition: .immediate) + + if inForeground { + Queue.mainQueue().after(0.5, { + guard let strongSelf = self, strongSelf.isVisibleInHierarchy else { + return + } + + strongSelf.deactivatePictureInPictureIfVisible.invoke(Void()) + }) + } } }) } @@ -705,6 +715,7 @@ public final class MediaStreamComponent: CombinedComponent { let toolbar = Child(ToolbarComponent.self) let activatePictureInPicture = StoredActionSlot(Action.self) + let deactivatePictureInPicture = StoredActionSlot(Void.self) let moreButtonTag = GenericComponentViewTag() let moreAnimationTag = GenericComponentViewTag() @@ -725,6 +736,16 @@ public final class MediaStreamComponent: CombinedComponent { let state = context.state let controller = environment.controller + context.state.deactivatePictureInPictureIfVisible.connect { + guard let controller = controller() else { + return + } + if controller.view.window == nil { + return + } + deactivatePictureInPicture.invoke(Void()) + } + let video = video.update( component: MediaStreamVideoComponent( call: context.component.call, @@ -733,6 +754,7 @@ public final class MediaStreamComponent: CombinedComponent { isAdmin: context.state.canManageCall, peerTitle: context.state.peerTitle, activatePictureInPicture: activatePictureInPicture, + deactivatePictureInPicture: deactivatePictureInPicture, bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in guard let call = call else { completed() @@ -742,6 +764,9 @@ public final class MediaStreamComponent: CombinedComponent { call.accountContext.sharedContext.mainWindow?.inCallNavigate?() completed() + }, + pictureInPictureClosed: { [weak call] in + let _ = call?.leave(terminateIfPossible: false) } ), availableSize: context.availableSize, @@ -1095,8 +1120,13 @@ public final class MediaStreamComponent: CombinedComponent { state.updateDismissOffset(value: offset.y, interactive: true) case let .ended(velocity): if abs(velocity.y) > 200.0 { - state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) - (controller() as? MediaStreamComponentController)?.dismiss(closing: false, manual: true) + activatePictureInPicture.invoke(Action { [weak state] in + guard let state = state, let controller = controller() as? MediaStreamComponentController else { + return + } + state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) + controller.dismiss(closing: false, manual: true) + }) } else { state.updateDismissOffset(value: 0.0, interactive: false) } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index f9d7bfc884..611b2ed3ca 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -14,16 +14,30 @@ final class MediaStreamVideoComponent: Component { let isAdmin: Bool let peerTitle: String let activatePictureInPicture: ActionSlot> + let deactivatePictureInPicture: ActionSlot let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void + let pictureInPictureClosed: () -> Void - init(call: PresentationGroupCallImpl, hasVideo: Bool, isVisible: Bool, isAdmin: Bool, peerTitle: String, activatePictureInPicture: ActionSlot>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) { + init( + call: PresentationGroupCallImpl, + hasVideo: Bool, + isVisible: Bool, + isAdmin: Bool, + peerTitle: String, + activatePictureInPicture: ActionSlot>, + deactivatePictureInPicture: ActionSlot, + bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void, + pictureInPictureClosed: @escaping () -> Void + ) { self.call = call self.hasVideo = hasVideo self.isVisible = isVisible self.isAdmin = isAdmin self.peerTitle = peerTitle self.activatePictureInPicture = activatePictureInPicture + self.deactivatePictureInPicture = deactivatePictureInPicture self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation + self.pictureInPictureClosed = pictureInPictureClosed } public static func ==(lhs: MediaStreamVideoComponent, rhs: MediaStreamVideoComponent) -> Bool { @@ -72,6 +86,8 @@ final class MediaStreamVideoComponent: Component { private var component: MediaStreamVideoComponent? private var hadVideo: Bool = false + private var requestedExpansion: Bool = false + private var noSignalTimer: Timer? private var noSignalTimeout: Bool = false @@ -101,7 +117,10 @@ final class MediaStreamVideoComponent: Component { } func expandFromPictureInPicture() { - self.pictureInPictureController?.stopPictureInPicture() + if let pictureInPictureController = self.pictureInPictureController, pictureInPictureController.isPictureInPictureActive { + self.requestedExpansion = true + self.pictureInPictureController?.stopPictureInPicture() + } } func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { @@ -290,6 +309,14 @@ final class MediaStreamVideoComponent: Component { completion(Void()) } + component.deactivatePictureInPicture.connect { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.expandFromPictureInPicture() + } + return availableSize } @@ -308,6 +335,14 @@ final class MediaStreamVideoComponent: Component { self.state?.updated(transition: .immediate) } + func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + if self.requestedExpansion { + self.requestedExpansion = false + } else { + self.component?.pictureInPictureClosed() + } + } + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { self.state?.updated(transition: .immediate) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f2004f17af..22e6876d83 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8848,7 +8848,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G updatedChatNavigationStack.removeSubrange(0 ..< (index + 1)) } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), animated: true, chatListFilter: nextFolderId, chatNavigationStack: updatedChatNavigationStack, completion: { nextController in + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), useBackAnimation: true, animated: true, chatListFilter: nextFolderId, chatNavigationStack: updatedChatNavigationStack, completion: { nextController in let _ = nextController })) }))) diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index ca8dd8a28a..4154be2ba1 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -122,9 +122,16 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam params.completion(controller) }) } else { - params.navigationController.replaceControllersAndPush(controllers: viewControllers, controller: controller, animated: params.animated, options: params.options, completion: { - params.completion(controller) - }) + if params.useBackAnimation { + params.navigationController.viewControllers = [controller] + params.navigationController.viewControllers + params.navigationController.replaceControllers(controllers: viewControllers + [controller], animated: params.animated, options: params.options, completion: { + params.completion(controller) + }) + } else { + params.navigationController.replaceControllersAndPush(controllers: viewControllers, controller: controller, animated: params.animated, options: params.options, completion: { + params.completion(controller) + }) + } } } if params.activateInput {