From d59f695e6f987bfb7107c0b4807f7c847a78f3aa Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 21 Sep 2020 20:54:32 +0300 Subject: [PATCH] Search filters improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + submodules/Camera/Sources/CameraOutput.swift | 4 + .../ChatListSearchRecentPeersNode.swift | 17 +- .../Sources/ChatListController.swift | 30 +- .../Sources/ChatListControllerNode.swift | 6 +- .../ChatListFilterTabContainerNode.swift | 2 - .../Sources/ChatListRecentPeersListItem.swift | 2 +- .../Sources/ChatListSearchContainerNode.swift | 2532 +++-------------- .../ChatListSearchFiltersContainerNode.swift | 152 +- .../Sources/ChatListSearchListPaneNode.swift | 2042 +++++++++++++ ...tListSearchMessageSelectionPanelNode.swift | 1 + .../ChatListSearchPaneContainerNode.swift | 539 ++++ .../ChatItemGalleryFooterContentNode.swift | 13 +- .../GalleryUI/Sources/GalleryController.swift | 32 +- .../GalleryUI/Sources/GalleryTitleView.swift | 62 + .../Sources/Items/ChatImageGalleryItem.swift | 31 +- .../Items/UniversalVideoGalleryItem.swift | 19 +- .../Sources/HashtagSearchController.swift | 7 +- .../InstantPageGalleryFooterContentNode.swift | 2 +- .../TGMediaPickerGalleryVideoItemView.m | 2 +- .../Sources/TGPhotoTextEntityView.m | 26 +- .../Sources/ListMessageFileItemNode.swift | 36 +- .../Sources/ListMessageItem.swift | 6 +- .../Sources/ListMessageSnippetItemNode.swift | 2 +- .../Sources/LocationActionListItem.swift | 4 +- .../Sources/LocationMapHeaderNode.swift | 4 +- .../AvatarGalleryItemFooterContentNode.swift | 2 +- .../Sources/SearchDisplayController.swift | 4 +- .../Themes/ThemeGridSelectionPanelNode.swift | 8 +- .../Themes/ThemePreviewController.swift | 2 +- .../Sources/Themes/WallpaperGalleryItem.swift | 2 +- .../ShareControllerRecentPeersGridItem.swift | 2 +- .../TelegramCore/Sources/SearchMessages.swift | 2 +- .../Sources/PresentationStrings.swift | 1591 +++++------ .../PresentationResourcesRootController.swift | 5 +- .../Sources/PeerDisplayName.swift | 35 + .../Resources/PresentationStrings.mapping | Bin 156303 -> 156342 bytes .../PeerInfo/PeerInfoPaneContainerNode.swift | 33 +- 38 files changed, 4080 insertions(+), 3180 deletions(-) create mode 100644 submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift create mode 100644 submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift create mode 100644 submodules/GalleryUI/Sources/GalleryTitleView.swift create mode 100644 submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3b7760a716..e637ddca55 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -5743,6 +5743,7 @@ Any member of this group will be able to see messages in the channel."; "Call.AccountIsLoggedOnCurrentDevice" = "Sorry, you can't call %@ because that account is logged in to Telegram on the device you're using for the call."; +"ChatList.Search.FilterChats" = "Chats"; "ChatList.Search.FilterMedia" = "Media"; "ChatList.Search.FilterLinks" = "Links"; "ChatList.Search.FilterFiles" = "Files"; diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index d2c4203b6a..537e465ba8 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -106,7 +106,11 @@ extension CameraOutput: AVCaptureMetadataOutputObjectsDelegate { func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { let codes: [CameraCode] = metadataObjects.filter { $0.type == .qr }.compactMap { object in if let object = object as? AVMetadataMachineReadableCodeObject, let stringValue = object.stringValue, !stringValue.isEmpty { + #if targetEnvironment(simulator) + return CameraCode(type: .qr, message: stringValue, corners: [CGPoint(), CGPoint(), CGPoint(), CGPoint()]) + #else return CameraCode(type: .qr, message: stringValue, corners: object.corners) + #endif } else { return nil } diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index 554abd7173..382b02bd16 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -109,7 +109,6 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { private var strings: PresentationStrings private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> private let mode: HorizontalPeerItemMode - private let sectionHeaderNode: ListSectionHeaderNode private let listView: ListView private let share: Bool @@ -133,15 +132,11 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { self.peerContextAction = peerContextAction self.isPeerSelected = isPeerSelected - self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) - self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople.uppercased() - self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init() - self.addSubnode(self.sectionHeaderNode) self.addSubnode(self.listView) let peersDisposable = DisposableSet() @@ -249,20 +244,14 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { self.theme = theme self.strings = strings self.themeAndStringsPromise.set(.single((self.theme, self.strings))) - - self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople.uppercased() - self.sectionHeaderNode.updateTheme(theme: theme) } } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 114.0) + return CGSize(width: constrainedSize.width, height: 86.0) } - public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 28.0)) - self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 28.0), leftInset: leftInset, rightInset: rightInset) - + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { var insets = UIEdgeInsets() insets.top += leftInset insets.bottom += rightInset @@ -277,7 +266,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: size.width) - self.listView.position = CGPoint(x: size.width / 2.0, y: 92.0 / 2.0 + 28.0) + self.listView.position = CGPoint(x: size.width / 2.0, y: 92.0 / 2.0) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: size.width), insets: insets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.itemCustomWidthValuePromise.set(itemCustomWidth) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index bfd25cf94f..6d3c3c47fa 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1682,40 +1682,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, scrollToTop() } - if let searchContentNode = strongSelf.searchContentNode { - var updatedDisplayFiltersPanelImpl: ((Bool) -> Void)? - - if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, navigationController: strongSelf.navigationController as? NavigationController, updatedDisplayFiltersPanel: { display in - updatedDisplayFiltersPanelImpl?(display) - }) { + if let searchContentNode = strongSelf.searchContentNode { + if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, navigationController: strongSelf.navigationController as? NavigationController) { let (filterContainerNode, activate) = filterContainerNodeAndActivate strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false) if let parentController = strongSelf.parent as? TabBarController { parentController.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: true) } activate() - - var currentDisplay = true - updatedDisplayFiltersPanelImpl = { [weak self, weak filterContainerNode] display in - guard let strongSelf = self, let strongFilterContainerNode = filterContainerNode else { - return - } - if currentDisplay != display { - currentDisplay = display - - let node = display ? strongFilterContainerNode : nil - strongSelf.navigationBar?.setSecondaryContentNode(node, animated: false) - if let parentController = strongSelf.parent as? TabBarController { - parentController.navigationBar?.setSecondaryContentNode(node, animated: true) - } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - if let layout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, transition: transition) - (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) - } - } - } } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index a700fbc752..9ec789289e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1147,7 +1147,7 @@ final class ChatListControllerNode: ASDisplayNode { } } - func activateSearch(placeholderNode: SearchBarPlaceholderNode, navigationController: NavigationController?, updatedDisplayFiltersPanel: ((Bool) -> Void)?) -> (ASDisplayNode, () -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode, navigationController: NavigationController?) -> (ASDisplayNode, () -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return nil } @@ -1169,9 +1169,7 @@ final class ChatListControllerNode: ASDisplayNode { self?.controller?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.controller?.presentInGlobalOverlay(c, with: a) - }, navigationController: navigationController, updatedDisplayFiltersPanel: { display in - updatedDisplayFiltersPanel?(display) - }) + }, navigationController: navigationController) self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: contentNode, cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 5424516ee6..2f4ffb40f8 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -837,8 +837,6 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height) - var previousFrame: CGRect? - var nextFrame: CGRect? var selectedFrame: CGRect? if let selectedFilter = selectedFilter, let currentIndex = reorderedFilters.firstIndex(where: { $0.id == selectedFilter }) { func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index a6a88dc501..b8908d7e35 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -94,7 +94,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { let currentItem = self.item return { [weak self] item, params, last in - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 124.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 96.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index e5ab68aff5..f676bf2f04 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -30,672 +30,70 @@ import InstantPageUI import ChatInterfaceState import ShareController -private enum ChatListRecentEntryStableId: Hashable { - case topPeers - case peerId(PeerId) -} - private enum ChatListTokenId: Int32 { case filter case peer case date } -private enum ChatListRecentEntry: Comparable, Identifiable { - case topPeers([Peer], PresentationTheme, PresentationStrings) - case peer(index: Int, peer: RecentlySearchedPeer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool) +final class ChatListSearchInteraction { + let openPeer: (Peer, Bool) -> Void + let openDisabledPeer: (Peer) -> Void + let openMessage: (Peer, MessageId) -> Void + let openUrl: (String) -> Void + let clearRecentSearch: () -> Void + let addContact: (String) -> Void + let toggleMessageSelection: (MessageId, Bool) -> Void + let messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void) + let mediaMessageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void) + let peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)? + let present: (ViewController, Any?) -> Void + let dismissInput: () -> Void + let updateSuggestedPeers: ([Peer]) -> Void + let getSelectedMessageIds: () -> Set? - var stableId: ChatListRecentEntryStableId { - switch self { - case .topPeers: - return .topPeers - case let .peer(_, peer, _, _, _, _, _, _): - return .peerId(peer.peer.peerId) - } - } - - static func ==(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool { - switch lhs { - case let .topPeers(lhsPeers, lhsTheme, lhsStrings): - if case let .topPeers(rhsPeers, rhsTheme, rhsStrings) = rhs { - if lhsPeers.count != rhsPeers.count { - return false - } - for i in 0 ..< lhsPeers.count { - if !lhsPeers[i].isEqual(rhsPeers[i]) { - return false - } - } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - return true - } else { - return false - } - case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsHasRevealControls): - if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsHasRevealControls) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsTimeFormat == rhsTimeFormat && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsHasRevealControls == rhsHasRevealControls { - return true - } else { - return false - } - } - } - - static func <(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool { - switch lhs { - case .topPeers: - return true - case let .peer(lhsIndex, _, _, _, _, _, _, _): - switch rhs { - case .topPeers: - return false - case let .peer(rhsIndex, _, _, _, _, _, _, _): - return lhsIndex <= rhsIndex - } - } - } - - func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disaledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { - switch self { - case let .topPeers(peers, theme, strings): - return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in - peerSelected(peer) - }, peerContextAction: { peer, node, gesture in - if let peerContextAction = peerContextAction { - peerContextAction(peer, .recentPeers, node, gesture) - } else { - gesture?.cancel() - } - }) - case let .peer(_, peer, theme, strings, timeFormat, nameSortOrder, nameDisplayOrder, hasRevealControls): - let primaryPeer: Peer - var chatPeer: Peer? - let maybeChatPeer = peer.peer.peers[peer.peer.peerId]! - if let associatedPeerId = maybeChatPeer.associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] { - primaryPeer = associatedPeer - chatPeer = maybeChatPeer - } else { - primaryPeer = maybeChatPeer - chatPeer = maybeChatPeer - } - - var enabled = true - if filter.contains(.onlyWriteable) { - if let peer = chatPeer { - enabled = canSendMessagesToPeer(peer) - } else { - enabled = canSendMessagesToPeer(primaryPeer) - } - } - if filter.contains(.onlyPrivateChats) { - if let peer = chatPeer { - if !(peer is TelegramUser || peer is TelegramSecretChat) { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let peer = chatPeer { - if let _ = peer as? TelegramGroup { - } else if let peer = peer as? TelegramChannel, case .group = peer.info { - } else { - enabled = false - } - } else { - enabled = false - } - } - - if filter.contains(.excludeChannels) { - if let channel = primaryPeer as? TelegramChannel, case .broadcast = channel.info { - enabled = false - } - } - - let status: ContactsPeerItemStatus - if let user = primaryPeer as? TelegramUser { - let servicePeer = isServicePeer(primaryPeer) - if user.flags.contains(.isSupport) && !servicePeer { - status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false) - } else if let _ = user.botInfo { - status = .custom(string: strings.Bot_GenericBotStatus, multiline: false) - } else if user.id != context.account.peerId && !servicePeer { - let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0) - status = .presence(presence, timeFormat) - } else { - status = .none - } - } else if let group = primaryPeer as? TelegramGroup { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false) - } else if let channel = primaryPeer as? TelegramChannel { - if case .group = channel.info { - if let count = peer.subpeerSummary?.count { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false) - } else { - status = .custom(string: strings.Group_Status, multiline: false) - } - } else { - if let count = peer.subpeerSummary?.count { - status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false) - } else { - status = .custom(string: strings.Channel_Status, multiline: false) - } - } - } else { - status = .none - } - - var isMuted = false - if let notificationSettings = peer.notificationSettings { - isMuted = notificationSettings.isRemovedFromTotalUnreadCount(default: false) - } - var badge: ContactsPeerItemBadge? - if peer.unreadCount > 0 { - badge = ContactsPeerItemBadge(count: peer.unreadCount, type: isMuted ? .inactive : .active) - } - - return ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { - clearRecentlySearchedPeers() - }), action: { _ in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerSelected(chatPeer) - } - }, disabledAction: { _ in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { - disaledPeerSelected(chatPeer) - } - }, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in - return { node, gesture in - if let chatPeer = peer.peer.peers[peer.peer.peerId], chatPeer.id.namespace != Namespaces.Peer.SecretChat { - peerContextAction(chatPeer, .recentSearch, node, gesture) - } else { - gesture?.cancel() - } - } - }) - } - } -} - -public enum ChatListSearchEntryStableId: Hashable { - case localPeerId(PeerId) - case globalPeerId(PeerId) - case messageId(MessageId) - case addContact - - public static func ==(lhs: ChatListSearchEntryStableId, rhs: ChatListSearchEntryStableId) -> Bool { - switch lhs { - case let .localPeerId(peerId): - if case .localPeerId(peerId) = rhs { - return true - } else { - return false - } - case let .globalPeerId(peerId): - if case .globalPeerId(peerId) = rhs { - return true - } else { - return false - } - case let .messageId(messageId): - if case .messageId(messageId) = rhs { - return true - } else { - return false - } - case .addContact: - if case .addContact = rhs { - return true - } else { - return false - } - } - } -} - -public enum ChatListSearchSectionExpandType { - case none - case expand - case collapse -} - -public enum ChatListSearchEntry: Comparable, Identifiable { - case localPeer(Peer, Peer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) - case message(Message, RenderedPeer, CombinedPeerReadState?, ChatListPresentationData, Int32, Bool?, Bool) - case addContact(String, PresentationTheme, PresentationStrings) - - 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, _, _, _, _, _, _): - return .messageId(message.id) - 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.isEqual(rhsPeer) && arePeersEqual(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): - if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = 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 - } - 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(lhsMessage, _, _, _, _, _, _): - if case let .message(rhsMessage, _, _, _, _, _, _) = rhs { - return lhsMessage.index < rhsMessage.index - } 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, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem { - switch self { - case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): - let primaryPeer: Peer - var chatPeer: Peer? - 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) - } else { - enabled = false - } - } - if filter.contains(.onlyPrivateChats) { - if let peer = chatPeer { - if !(peer is TelegramUser || peer is TelegramSecretChat) { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let peer = chatPeer { - if let _ = peer as? TelegramGroup { - } else if let peer = peer as? TelegramChannel, 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 actionTitle: String? - switch expandType { - case .none: - actionTitle = nil - case .expand: - actionTitle = strings.ChatList_Search_ShowMore - case .collapse: - actionTitle = strings.ChatList_Search_ShowLess - } - header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { - toggleExpandLocalResults() - }) - } - - 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: { _ in - interaction.peerSelected(peer, nil) - }, contextAction: peerContextAction.flatMap { peerContextAction in - return { node, gesture in - if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat { - peerContextAction(chatPeer, .search, node, gesture) - } else { - gesture?.cancel() - } - } - }, arrowAction: nil) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): - var enabled = true - if filter.contains(.onlyWriteable) { - enabled = canSendMessagesToPeer(peer.peer) - } - if filter.contains(.onlyPrivateChats) { - if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let _ = peer.peer as? TelegramGroup { - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { - } else { - enabled = false - } - } - - var suffixString = "" - if let subscribers = peer.subscribers, subscribers != 0 { - if peer.peer is TelegramUser { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" - } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" - } else { - suffixString = ", \(strings.Conversation_StatusMembers(subscribers))" - } - } - - 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 actionTitle: String? - switch expandType { - case .none: - actionTitle = nil - case .expand: - actionTitle = strings.ChatList_Search_ShowMore - case .collapse: - actionTitle = strings.ChatList_Search_ShowLess - } - header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { - toggleExpandGlobalResults() - }) - } - - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in - interaction.peerSelected(peer.peer, nil) - }, contextAction: peerContextAction.flatMap { peerContextAction in - return { node, gesture in - peerContextAction(peer.peer, .search, node, gesture) - } - }) - case let .message(message, peer, readState, presentationData, totalCount, selected, displayCustomHeader): - let header = ChatListSearchItemHeader(type: .messages(totalCount), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) - let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none - if let tags = searchOptions?.messageTags, tags != .photoOrVideo { - return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tags == .webPage, isGlobalSearchResult: true) - } else { - return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction) - } - case let .addContact(phoneNumber, theme, strings): - return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { - interaction.addContact(phoneNumber) - }) - } - } -} - -private struct ChatListSearchContainerRecentTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -public struct ChatListSearchContainerTransition { - public let deletions: [ListViewDeleteItem] - public let insertions: [ListViewInsertItem] - public let updates: [ListViewUpdateItem] - public let displayingResults: Bool - public let isEmpty: Bool - public let isLoading: Bool - public let query: String - public let animated: Bool - - public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String, animated: Bool) { - self.deletions = deletions - self.insertions = insertions - self.updates = updates - self.displayingResults = displayingResults - self.isEmpty = isEmpty - self.isLoading = isLoading - self.query = query - self.animated = animated - } -} - -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disaledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { - 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, filter: filter, peerSelected: peerSelected, disaledPeerSelected: disaledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disaledPeerSelected: disaledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } - - 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, searchQuery: String, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> 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, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } - - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) -} - -private struct ChatListSearchContainerNodeState: Equatable { - let peerIdWithRevealedOptions: PeerId? - - - init(peerIdWithRevealedOptions: PeerId? = nil) { - self.peerIdWithRevealedOptions = peerIdWithRevealedOptions - } - - static func ==(lhs: ChatListSearchContainerNodeState, rhs: ChatListSearchContainerNodeState) -> Bool { - if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { - return false - } - return true - } - - func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListSearchContainerNodeState { - return ChatListSearchContainerNodeState(peerIdWithRevealedOptions: peerIdWithRevealedOptions) + init(openPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (MessageId, Bool) -> Void, messageContextAction: @escaping ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), mediaMessageContextAction: @escaping ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, updateSuggestedPeers: @escaping ([Peer]) -> Void, getSelectedMessageIds: @escaping () -> Set?) { + self.openPeer = openPeer + self.openDisabledPeer = openDisabledPeer + self.openMessage = openMessage + self.openUrl = openUrl + self.clearRecentSearch = clearRecentSearch + self.addContact = addContact + self.toggleMessageSelection = toggleMessageSelection + self.messageContextAction = messageContextAction + self.mediaMessageContextAction = mediaMessageContextAction + self.peerContextAction = peerContextAction + self.present = present + self.dismissInput = dismissInput + self.updateSuggestedPeers = updateSuggestedPeers + self.getSelectedMessageIds = getSelectedMessageIds } } private struct ChatListSearchContainerNodeSearchState: Equatable { - var expandLocalSearch: Bool = false - var expandGlobalSearch: Bool = false var selectedMessageIds: Set? func withUpdatedSelectedMessageIds(_ selectedMessageIds: Set?) -> ChatListSearchContainerNodeSearchState { - return ChatListSearchContainerNodeSearchState(expandLocalSearch: self.expandLocalSearch, expandGlobalSearch: self.expandGlobalSearch, selectedMessageIds: selectedMessageIds) - } -} - -private func doesPeerMatchFilter(peer: Peer, filter: ChatListNodePeersFilter) -> Bool { - var enabled = true - if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { - enabled = false - } - if filter.contains(.onlyPrivateChats), !(peer is TelegramUser || peer is TelegramSecretChat) { - enabled = false - } - if filter.contains(.onlyGroups) { - if let _ = peer as? TelegramGroup { - } else if let peer = peer as? TelegramChannel, case .group = peer.info { - } else { - enabled = false - } - } - return enabled -} - -private struct ChatListSearchMessagesResult { - let query: String - let messages: [Message] - let readStates: [PeerId: CombinedPeerReadState] - let hasMore: Bool - let totalCount: Int32 - let state: SearchMessagesState -} - -private struct ChatListSearchMessagesContext { - let result: ChatListSearchMessagesResult - let loadMoreIndex: MessageIndex? -} - -public enum ChatListSearchContextActionSource { - case recentPeers - case recentSearch - case search -} - -public struct ChatListSearchOptions { - let peer: (PeerId, Bool, String)? - let minDate: (Int32, String)? - let maxDate: (Int32, String)? - let messageTags: MessageTags? - - var isEmpty: Bool { - return self.peer == nil && self.minDate == nil && self.maxDate == nil && self.messageTags == nil - } - - func withUpdatedPeer(_ peerIdIsGroupAndName: (PeerId, Bool, String)?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: peerIdIsGroupAndName, minDate: self.minDate, maxDate: self.maxDate, messageTags: self.messageTags) - } - - func withUpdatedMinDate(_ minDateAndTitle: (Int32, String)?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: self.peer, minDate: minDateAndTitle, maxDate: self.maxDate, messageTags: self.messageTags) - } - - func withUpdatedMaxDate(_ maxDateAndTitle: (Int32, String)?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: self.peer, minDate: self.minDate, maxDate: maxDateAndTitle, messageTags: self.messageTags) - } - - func withUpdatedMessageTags(_ messageTags: MessageTags?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: self.peer, minDate: self.minDate, maxDate: self.maxDate, messageTags: messageTags) + return ChatListSearchContainerNodeSearchState(selectedMessageIds: selectedMessageIds) } } public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext private let peersFilter: ChatListNodePeersFilter - private var interaction: ChatListNodeInteraction? + private var interaction: ChatListSearchInteraction? private let openMessage: (Peer, MessageId) -> Void private let navigationController: NavigationController? let filterContainerNode: ChatListSearchFiltersContainerNode + private let paneContainerNode: ChatListSearchPaneContainerNode private var selectionPanelNode: ChatListSearchMessageSelectionPanelNode? - private let recentListNode: ListView - private let loadingNode: ASImageNode - private let listNode: ListView - private let mediaNode: ChatListSearchMediaNode - private let dimNode: ASDisplayNode - private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = [] - private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] - private var validLayout: (ContainerViewLayout, CGFloat)? private var present: ((ViewController, Any?) -> Void)? private var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? private let activeActionDisposable = MetaDisposable() - - private let recentDisposable = MetaDisposable() - private let updatedRecentPeersDisposable = MetaDisposable() - + private var searchQueryValue: String? private let searchQuery = Promise(nil) private var searchOptionsValue: ChatListSearchOptions? @@ -705,501 +103,58 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let presentationDataPromise: Promise - private var stateValue = ChatListSearchContainerNodeState() - private let statePromise: ValuePromise - private var searchStateValue = ChatListSearchContainerNodeSearchState() - private let searchStatePromise: ValuePromise - private let searchContextValue = Atomic(value: nil) - private var searchCurrentMessages: [Message]? - private let suggestedDates = Promise<[(Date, String?)]>([]) - private var suggestedDatesValue: [(Date, String?)] = [] { - didSet { - self.suggestedDates.set(.single(self.suggestedDatesValue)) - } - } private let suggestedPeers = Promise<[Peer]>([]) - private var suggestedPeersValue: [Peer] = [] { - didSet { - self.suggestedPeers.set(.single(self.suggestedPeersValue)) - } - } - private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() - private let _isSearching = ValuePromise(false, ignoreRepeated: true) - override public var isSearching: Signal { - return self._isSearching.get() + private var stateValue = ChatListSearchContainerNodeSearchState() + private let statePromise = ValuePromise() + + private var selectedFilterKey: ChatListSearchFilterEntryId? = .filter(ChatListSearchFilter.chats.id) + private var transitionFraction: CGFloat = 0.0 + + private var didSetReady: Bool = false + private let _ready = Promise() + public override func ready() -> Signal { + return self._ready.get() } - private var mediaStatusDisposable: Disposable? - private var playlistPreloadDisposable: Disposable? + private var validLayout: (ContainerViewLayout, CGFloat)? - private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? - private var mediaAccessoryPanelContainer: PassthroughContainerNode - private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? - private var dismissingPanel: ASDisplayNode? - - private let updatedDisplayFiltersPanel: ((Bool) -> Void)? - - private let emptyResultsTitleNode: ImmediateTextNode - private let emptyResultsTextNode: ImmediateTextNode - private let emptyResultsAnimationNode: AnimatedStickerNode - private var animationSize: CGSize = CGSize() - - public init(context: AccountContext, filter: ChatListNodePeersFilter, groupId: PeerGroupId, openPeer originalOpenPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage originalOpenMessage: @escaping (Peer, MessageId) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?, updatedDisplayFiltersPanel: ((Bool) -> Void)? = nil) { + public init(context: AccountContext, filter: ChatListNodePeersFilter, groupId: PeerGroupId, openPeer originalOpenPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage originalOpenMessage: @escaping (Peer, MessageId) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { self.context = context self.peersFilter = filter - self.dimNode = ASDisplayNode() self.navigationController = navigationController - self.updatedDisplayFiltersPanel = updatedDisplayFiltersPanel - - self.present = present - self.presentInGlobalOverlay = presentInGlobalOverlay + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.openMessage = originalOpenMessage + self.present = present + self.presentInGlobalOverlay = presentInGlobalOverlay - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) - self.filterContainerNode = ChatListSearchFiltersContainerNode() - - self.recentListNode = ListView() - self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - - var openMediaMessageImpl: ((Message, ChatControllerInteractionOpenMessageMode) -> Void)? - var messageContextActionImpl: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)? - var toggleMessageSelectionImpl: ((MessageId, Bool) -> Void)? - var transitionNodeImpl: ((MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?)? - var addToTransitionSurfaceImpl: ((UIView) -> Void)? - - self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in - openMediaMessageImpl?(message, mode) - }, messageContextAction: { message, sourceNode, sourceRect, gesture in - messageContextActionImpl?(message, sourceNode, sourceRect, gesture) - }, toggleMessageSelection: { messageId, selected in - toggleMessageSelectionImpl?(messageId, selected) - }) - - self.loadingNode = ASImageNode() - - self.listNode = ListView() - self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - - self.statePromise = ValuePromise(self.stateValue, ignoreRepeated: true) - self.searchStatePromise = ValuePromise(self.searchStateValue, ignoreRepeated: true) - - self.mediaAccessoryPanelContainer = PassthroughContainerNode() - self.mediaAccessoryPanelContainer.clipsToBounds = true - - self.emptyResultsTitleNode = ImmediateTextNode() - self.emptyResultsTitleNode.displaysAsynchronously = false - self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) - self.emptyResultsTitleNode.textAlignment = .center - self.emptyResultsTitleNode.isHidden = true - - self.emptyResultsTextNode = ImmediateTextNode() - self.emptyResultsTextNode.displaysAsynchronously = false - self.emptyResultsTextNode.maximumNumberOfLines = 0 - self.emptyResultsTextNode.textAlignment = .center - self.emptyResultsTextNode.isHidden = true - - self.emptyResultsAnimationNode = AnimatedStickerNode() - self.emptyResultsAnimationNode.isHidden = true + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, peersFilter: self.peersFilter, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode.clipsToBounds = true super.init() - - if let path = getAppBundle().path(forResource: "ChatListNoResults", ofType: "tgs") { - self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - self.animationSize = CGSize(width: 124.0, height: 124.0) - } - - self.dimNode.backgroundColor = filter.contains(.excludeRecent) ? UIColor.black.withAlphaComponent(0.5) : self.presentationData.theme.chatList.backgroundColor - + self.backgroundColor = filter.contains(.excludeRecent) ? nil : self.presentationData.theme.chatList.backgroundColor - self.addSubnode(self.dimNode) - self.addSubnode(self.recentListNode) - self.addSubnode(self.listNode) - self.addSubnode(self.loadingNode) - self.addSubnode(self.mediaNode) - - self.addSubnode(self.mediaAccessoryPanelContainer) - - self.addSubnode(self.emptyResultsAnimationNode) - self.addSubnode(self.emptyResultsTitleNode) - self.addSubnode(self.emptyResultsTextNode) + self.addSubnode(self.paneContainerNode) - let searchContext = Promise(nil) - let searchContextValue = self.searchContextValue - let updateSearchContext: ((ChatListSearchMessagesContext?) -> (ChatListSearchMessagesContext?, Bool)) -> Void = { f in - var shouldUpdate = false - let updated = searchContextValue.modify { current in - let (u, s) = f(current) - shouldUpdate = s - if s { - return u - } else { - return current - } + let interaction = ChatListSearchInteraction(openPeer: { peer, value in + originalOpenPeer(peer, value) + if peer.id.namespace != Namespaces.Peer.SecretChat { + addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_peer", peerId: peer.id) } - if shouldUpdate { - searchContext.set(.single(updated)) + }, openDisabledPeer: { peer in + openDisabledPeer(peer) + }, openMessage: { peer, messageId in + originalOpenMessage(peer, messageId) + if peer.id.namespace != Namespaces.Peer.SecretChat { + addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) } - } - - self.listNode.isHidden = true - self.mediaNode.isHidden = true - self.recentListNode.isHidden = filter.contains(.excludeRecent) - - let currentRemotePeers = Atomic<([FoundPeer], [FoundPeer])?>(value: nil) - let presentationDataPromise = self.presentationDataPromise - let searchStatePromise = self.searchStatePromise - let foundItems = combineLatest(self.searchQuery.get(), self.searchOptions.get()) - |> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in - if query == nil && options == nil { - let _ = currentRemotePeers.swap(nil) - return .single(nil) - } - - let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) - |> take(1) - - let foundLocalPeers: Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> - - if let query = query { - foundLocalPeers = context.account.postbox.searchPeers(query: query.lowercased()) - |> mapToSignal { local -> Signal<([PeerView], [RenderedPeer]), NoError> in - return combineLatest(local.map { context.account.postbox.peerView(id: $0.peerId) }) |> map { views in - return (views, local) - } - } - |> mapToSignal { viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in - return context.account.postbox.unreadMessageCountsView(items: viewsAndPeers.0.map {.peer($0.peerId)}) |> map { values in - var unread: [PeerId: (Int32, Bool)] = [:] - for peerView in viewsAndPeers.0 { - var isMuted: Bool = false - if let nofiticationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - switch nofiticationSettings.muteState { - case .muted: - isMuted = true - default: - break - } - } - - let unreadCount = values.count(for: .peer(peerView.peerId)) - if let unreadCount = unreadCount, unreadCount > 0 { - unread[peerView.peerId] = (unreadCount, isMuted) - } - } - return (peers: viewsAndPeers.1, unread: unread) - } - } - } else { - foundLocalPeers = .single((peers: [], unread: [:])) - } - - let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> - let currentRemotePeersValue = currentRemotePeers.with { $0 } ?? ([], []) - if let query = query { - foundRemotePeers = ( - .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) - |> then( - searchPeers(account: context.account, query: query) - |> map { ($0.0, $0.1, false) } - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - ) - ) - } else { - foundRemotePeers = .single(([], [], false)) - } - let location: SearchMessagesLocation - if let options = options { - if let (peerId, _, _) = options.peer { - location = .peer(peerId: peerId, fromId: nil, tags: options.messageTags, topMsgId: nil, minDate: options.minDate?.0, maxDate: options.maxDate?.0) - } else { - - location = .general(tags: options.messageTags, minDate: options.minDate?.0, maxDate: options.maxDate?.0) - } - } else { - location = .general(tags: nil, minDate: nil, maxDate: nil) - } - - let finalQuery = query ?? "" - updateSearchContext { _ in - return (nil, true) - } - let foundRemoteMessages: Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> - if filter.contains(.doNotSearchMessages) { - foundRemoteMessages = .single((([], [:], 0), false)) - } else { - if !finalQuery.isEmpty { - addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") - } - - let searchSignal = searchMessages(account: context.account, location: location, query: finalQuery, state: nil, limit: 50) - |> map { result, updatedState -> ChatListSearchMessagesResult in - return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) - } - - let loadMore = searchContext.get() - |> mapToSignal { searchContext -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in - if let searchContext = searchContext, searchContext.result.hasMore { - if let _ = searchContext.loadMoreIndex { - return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80) - |> map { result, updatedState -> ChatListSearchMessagesResult in - return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) - } - |> mapToSignal { foundMessages -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in - updateSearchContext { previous in - let updated = ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil) - return (updated, true) - } - return .complete() - } - } else { - return .single(((searchContext.result.messages, searchContext.result.readStates, searchContext.result.totalCount), false)) - } - } else { - return .complete() - } - } - - foundRemoteMessages = .single((([], [:], 0), true)) - |> then( - searchSignal - |> map { foundMessages -> (([Message], [PeerId: CombinedPeerReadState], Int32), Bool) in - updateSearchContext { _ in - return (ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil), true) - } - return ((foundMessages.messages, foundMessages.readStates, foundMessages.totalCount), false) - } - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - |> then(loadMore) - ) - } - - let resolvedMessage = .single(nil) - |> then(context.sharedContext.resolveUrl(account: context.account, url: finalQuery) - |> mapToSignal { resolvedUrl -> Signal in - if case let .channelMessage(_, messageId) = resolvedUrl { - return downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: messageId) - } else { - return .single(nil) - } - }) - - return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), resolvedMessage) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, resolvedMessage -> ([ChatListSearchEntry], Bool)? in - let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 - var entries: [ChatListSearchEntry] = [] - var index = 0 - - let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1)) - - let filteredPeer:(Peer, Peer) -> Bool = { peer, accountPeer in - guard !filter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } - guard !filter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false } - guard !filter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } - - if filter.contains(.onlyGroups) { - var isGroup: Bool = false - if let peer = peer as? TelegramChannel, case .group = peer.info { - isGroup = true - } else if peer.id.namespace == Namespaces.Peer.CloudGroup { - isGroup = true - } - if !isGroup { - return false - } - } - - if filter.contains(.onlyChannels) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - return true - } else { - return false - } - } - - if filter.contains(.excludeChannels) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - return false - } - } - - return true - } - - var existingPeerIds = Set() - - var totalNumberOfLocalPeers = 0 - for renderedPeer in foundLocalPeers.peers { - if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { - if !existingPeerIds.contains(peer.id) { - existingPeerIds.insert(peer.id) - totalNumberOfLocalPeers += 1 - } - } - } - for peer in foundRemotePeers.0 { - if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { - existingPeerIds.insert(peer.peer.id) - totalNumberOfLocalPeers += 1 - } - } - - var totalNumberOfGlobalPeers = 0 - for peer in foundRemotePeers.1 { - if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { - totalNumberOfGlobalPeers += 1 - } - } - - existingPeerIds.removeAll() - - let localExpandType: ChatListSearchSectionExpandType - if let _ = options?.messageTags { - if totalNumberOfLocalPeers > 3 { - localExpandType = searchState.expandLocalSearch ? .collapse : .expand - } else { - localExpandType = .none - } - } else { - localExpandType = .none - } - let globalExpandType: ChatListSearchSectionExpandType - if totalNumberOfGlobalPeers > 3 { - globalExpandType = searchState.expandGlobalSearch ? .collapse : .expand - } else { - globalExpandType = .none - } - - let lowercasedQuery = finalQuery.lowercased() - if lowercasedQuery.count > 1 && (presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery)) { - if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) { - existingPeerIds.insert(accountPeer.id) - entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) - index += 1 - } - } - - var numberOfLocalPeers = 0 - for renderedPeer in foundLocalPeers.peers { - if case .expand = localExpandType, numberOfLocalPeers >= 3 { - break - } - - if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { - if !existingPeerIds.contains(peer.id) { - existingPeerIds.insert(peer.id) - var associatedPeer: Peer? - if let associatedPeerId = peer.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 - } - } - } - - for peer in foundRemotePeers.0 { - if case .expand = localExpandType, numberOfLocalPeers >= 3 { - break - } - - if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { - existingPeerIds.insert(peer.peer.id) - entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) - index += 1 - numberOfLocalPeers += 1 - } - } - - var numberOfGlobalPeers = 0 - index = 0 - if let _ = options?.messageTags { - } else { - for peer in foundRemotePeers.1 { - if case .expand = globalExpandType, numberOfGlobalPeers >= 3 { - break - } - - if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { - existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) - index += 1 - numberOfGlobalPeers += 1 - } - } - } - - if let message = resolvedMessage { - var peer = RenderedPeer(message: message) - if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { - if let channelPeer = message.peers[migrationReference.peerId] { - peer = RenderedPeer(peer: channelPeer) - } - } - entries.append(.message(message, peer, nil, presentationData, 1, nil, true)) - index += 1 - } - - var firstHeaderId: Int64? - if !foundRemotePeers.2 { - index = 0 - for message in foundRemoteMessages.0.0 { - let headerId = listMessageDateHeaderId(timestamp: message.timestamp) - if firstHeaderId == nil { - firstHeaderId = headerId - } - var peer = RenderedPeer(message: message) - if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { - if let channelPeer = message.peers[migrationReference.peerId] { - peer = RenderedPeer(peer: channelPeer) - } - } - entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, searchState.selectedMessageIds?.contains(message.id), headerId == firstHeaderId)) - index += 1 - } - } - - if let _ = addContact, isViablePhoneNumber(finalQuery) { - entries.append(.addContact(finalQuery, presentationData.theme, presentationData.strings)) - } - - return (entries, isSearching) - } - } - - let foundMessages = searchContext.get() |> map { searchContext -> ([Message], Int32, Bool) in - if let result = searchContext?.result { - return (result.messages, result.totalCount, result.hasMore) - } else { - return ([], 0, false) - } - } - - let loadMore = { - updateSearchContext { previous in - guard let previous = previous else { - return (nil, false) - } - if previous.loadMoreIndex != nil { - return (previous, false) - } - guard let last = previous.result.messages.last else { - return (previous, false) - } - return (ChatListSearchMessagesContext(result: previous.result, loadMoreIndex: last.index), true) - } - } - - let openUrlImpl: (String) -> Void = { url in + }, openUrl: { url in openUserGeneratedUrl(context: context, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in @@ -1213,37 +168,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self?.dismissInput() }, contentContext: nil) }) - } - - openMediaMessageImpl = { [weak self] message, mode in - let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { - self?.dismissInput() - }, present: { c, a in - present(c, a) - }, transitionNode: { messageId, media in - return transitionNodeImpl?(messageId, media) - }, addToTransitionSurface: { view in - addToTransitionSurfaceImpl?(view) - }, openUrl: { url in - openUrlImpl(url) - }, openPeer: { peer, navigation in - //self?.openPeer(peerId: peer.id, navigation: navigation) - }, callPeer: { _, _ in - }, enqueueMessage: { _ in - }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, gallerySource: .custom(messages: foundMessages, messageId: message.id, loadMore: { - loadMore() - }))) - } - - messageContextActionImpl = { [weak self] message, sourceNode, sourceRect, gesture in - if let strongSelf = self { - strongSelf.messageContextActions(message, node: sourceNode, rect: sourceRect, gesture: gesture) + }, clearRecentSearch: { [weak self] in + guard let strongSelf = self else { + return } - } - - toggleMessageSelectionImpl = { [weak self] messageId, selected in + let presentationData = strongSelf.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.WebSearch_RecentSectionClear, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + let _ = (clearRecentlySearchedPeers(postbox: strongSelf.context.account.postbox) + |> deliverOnMainQueue).start() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.dismissInput() + strongSelf.present?(actionSheet, nil) + }, addContact: { phoneNumber in + addContact?(phoneNumber) + }, toggleMessageSelection: { [weak self] messageId, selected in if let strongSelf = self { - strongSelf.updateSearchState { state in + strongSelf.updateState { state in var selectedMessageIds = state.selectedMessageIds ?? Set() if selected { selectedMessageIds.insert(messageId) @@ -1253,415 +205,94 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return state.withUpdatedSelectedMessageIds(selectedMessageIds) } } - } - - transitionNodeImpl = { [weak self] messageId, media in + }, messageContextAction: { [weak self] message, node, rect, gesture in + self?.messageContextAction(message, node: node, rect: rect, gesture: gesture) + }, mediaMessageContextAction: { [weak self] message, node, rect, gesture in + self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture) + }, peerContextAction: { peer, source, node, gesture in + peerContextAction?(peer, source, node, gesture) + }, present: { c, a in + present(c, a) + }, dismissInput: { [weak self] in + self?.dismissInput() + }, updateSuggestedPeers: { [weak self] peers in + self?.suggestedPeers.set(.single(peers)) + }, getSelectedMessageIds: { [weak self] () -> Set? in if let strongSelf = self { - return strongSelf.mediaNode.transitionNodeForGallery(messageId: messageId, media: media) + return strongSelf.stateValue.selectedMessageIds } else { return nil } - } - - addToTransitionSurfaceImpl = { [weak self] view in - if let strongSelf = self { - strongSelf.mediaNode.addToTransitionSurface(view: view) - } - } - - let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) - - let openPeer: (Peer, Bool) -> Void = { peer, value in - originalOpenPeer(peer, value) - - if peer.id.namespace != Namespaces.Peer.SecretChat { - addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_peer", peerId: peer.id) - } - } - - let openMessage: (Peer, MessageId) -> Void = { peer, messageId in - originalOpenMessage(peer, messageId) - - if peer.id.namespace != Namespaces.Peer.SecretChat { - addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) - } - } - - let interaction = ChatListNodeInteraction(activateSearch: { - }, peerSelected: { [weak self] peer, _ in - self?.dismissInput() - openPeer(peer, false) - let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() - self?.listNode.clearHighlightAnimated(true) - }, disabledPeerSelected: { _ in - }, togglePeerSelected: { _ in - }, additionalCategorySelected: { _ in - }, messageSelected: { [weak self] peer, message, _ in - self?.dismissInput() - if let peer = message.peers[message.id.peerId] { - openMessage(peer, message.id) - } - self?.listNode.clearHighlightAnimated(true) - }, groupSelected: { _ in - }, addContact: { [weak self] phoneNumber in - self?.dismissInput() - addContact?(phoneNumber) - self?.listNode.clearHighlightAnimated(true) - }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in - if let strongSelf = self { - strongSelf.updateState { state in - if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { - return state.withUpdatedPeerIdWithRevealedOptions(peerId) - } else { - return state - } - } - } - }, setItemPinned: { _, _ in - }, setPeerMuted: { _, _ in - }, deletePeer: { _, _ in - }, updatePeerGrouping: { _, _ in - }, togglePeerMarkedUnread: { _, _ in - }, toggleArchivedFolderHiddenByDefault: { - }, hidePsa: { _ in - }, activateChatPreview: { item, node, gesture in - guard let peerContextAction = peerContextAction else { - gesture?.cancel() - return - } - switch item.content { - case let .peer(peer): - if let peer = peer.peer.peer { - peerContextAction(peer, .search, node, gesture) - } - case .groupReference: - gesture?.cancel() - } - }, present: { c in - present(c, nil) }) - self.interaction = interaction + self.paneContainerNode.interaction = interaction - let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let hasRecentPeers = recentPeers(account: context.account) - |> map { value -> Bool in - switch value { - case let .peers(peers): - return !peers.isEmpty - case .disabled: - return false - } - } - |> distinctUntilChanged - - let previousRecentlySearchedPeerOrder = Atomic<[PeerId]>(value: []) - let fixedRecentlySearchedPeers = recentlySearchedPeers(postbox: context.account.postbox) - |> map { peers -> [RecentlySearchedPeer] in - var result: [RecentlySearchedPeer] = [] - let _ = previousRecentlySearchedPeerOrder.modify { current in - var updated: [PeerId] = [] - for id in current { - inner: for peer in peers { - if peer.peer.peerId == id { - updated.append(id) - result.append(peer) - break inner - } - } - } - for peer in peers.reversed() { - if !updated.contains(peer.peer.peerId) { - updated.insert(peer.peer.peerId, at: 0) - result.insert(peer, at: 0) - } - } - return updated - } - return result - } - - var recentItems = combineLatest(hasRecentPeers, fixedRecentlySearchedPeers, presentationDataPromise.get(), self.statePromise.get()) - |> mapToSignal { hasRecentPeers, peers, presentationData, state -> Signal<[ChatListRecentEntry], NoError> in - var entries: [ChatListRecentEntry] = [] - if !filter.contains(.onlyGroups) { - if hasRecentPeers { - entries.append(.topPeers([], presentationData.theme, presentationData.strings)) - } - } - var peerIds = Set() - var index = 0 - loop: for searchedPeer in peers { - if let peer = searchedPeer.peer.peers[searchedPeer.peer.peerId] { - if peerIds.contains(peer.id) { - continue loop - } - if !doesPeerMatchFilter(peer: peer, filter: filter) { - continue - } - peerIds.insert(peer.id) - - entries.append(.peer(index: index, peer: searchedPeer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder, state.peerIdWithRevealedOptions == peer.id)) - index += 1 - } - } - - return .single(entries) - } - - if filter.contains(.excludeRecent) { - recentItems = .single([]) - } - - self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) - - self.recentDisposable.set((combineLatest(queue: .mainQueue(), - presentationDataPromise.get(), - recentItems - ) - |> deliverOnMainQueue).start(next: { [weak self] presentationData, entries in + self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in if let strongSelf = self { - let previousEntries = previousRecentItems.swap(entries) - - let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: filter, peerSelected: { peer in - openPeer(peer, true) - let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() - self?.recentListNode.clearHighlightAnimated(true) - }, disaledPeerSelected: { peer in - openDisabledPeer(peer) - }, peerContextAction: peerContextAction, - clearRecentlySearchedPeers: { - self?.clearRecentSearch() - }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in - interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId) - }, deletePeer: { peerId in - if let strongSelf = self { - let _ = removeRecentlySearchedPeer(postbox: strongSelf.context.account.postbox, peerId: peerId).start() - } - }) - strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) - } - })) - - let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in - self?.dismissInput() - return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { [weak self] in - self?.dismissInput() - }, present: { c, a in - present(c, a) - }, transitionNode: { [weak self] messageId, media in - var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? - if let strongSelf = self { - strongSelf.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ListMessageNode { - if let result = itemNode.transitionNode(id: messageId, media: media) { - transitionNode = result - } - } + var filterKey: ChatListSearchFilter? + if let key = key { + switch key { + case .chats: + filterKey = .chats + case .media: + filterKey = .media + case .links: + filterKey = .links + case .files: + filterKey = .files + case .music: + filterKey = .music + case .voice: + filterKey = .voice } } - return transitionNode - }, addToTransitionSurface: { view in - self?.view.addSubview(view) - }, openUrl: { url in - openUrlImpl(url) - }, openPeer: { peer, navigation in - // self?.openPeer(peerId: peer.id, navigation: navigation) - }, callPeer: { _, _ in - }, enqueueMessage: { _ in - }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .custom(messages: foundMessages, at: message.id, loadMore: { - loadMore() - }), gallerySource: .custom(messages: foundMessages, messageId: message.id, loadMore: { - loadMore() - }))) - }, openMessageContextMenu: { [weak self] message, bool, node, rect, gesture in - self?.messageContextAction(message, node: node, rect: rect, gesture: gesture) - }, toggleMessagesSelection: { messageId, selected in - if let messageId = messageId.first { - toggleMessageSelectionImpl?(messageId, selected) - } - }, openUrl: { url, _, _, message in - openUrlImpl(url) - }, openInstantPage: { message, data in - if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let pageController = InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor) - navigationController?.pushViewController(pageController) - } - }, longTap: { action, message in - }, getHiddenMedia: { - return [:] - }) - - let previousSearchState = Atomic(value: nil) - self.searchDisposable.set((foundItems - |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags in - if let strongSelf = self { - let previousState = previousSearchState.swap(strongSelf.searchStateValue) + strongSelf.selectedFilterKey = filterKey.flatMap { .filter($0.id) } + strongSelf.transitionFraction = transitionFraction - let isSearching = entriesAndFlags?.1 ?? false - strongSelf._isSearching.set(isSearching) - - if strongSelf.searchOptionsValue?.messageTags == .photoOrVideo { - var totalCount: Int32 = 0 - if let entries = entriesAndFlags?.0 { - for entry in entries { - if case let .message(_, _, _, _, count, _, _) = entry { - totalCount = count - break - } - } - } - var entries: [ChatListSearchEntry]? = entriesAndFlags?.0 ?? [] - if isSearching && (entries?.isEmpty ?? true) { - entries = nil - } - strongSelf.mediaNode.updateHistory(entries: entries, totalCount: totalCount, updateType: .Initial) - } - - var entriesAndFlags = entriesAndFlags - - var peers: [Peer] = [] - if let entries = entriesAndFlags?.0 { - var filteredEntries: [ChatListSearchEntry] = [] - for entry in entries { - if case let .localPeer(peer, _, _, _, _, _, _, _, _) = entry { - peers.append(peer) - } else { - filteredEntries.append(entry) - } - } - - if strongSelf.searchOptionsValue?.messageTags != nil || strongSelf.searchOptionsValue?.maxDate != nil || strongSelf.searchOptionsValue?.peer != nil { - entriesAndFlags?.0 = filteredEntries - } - } - - let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) - let newEntries = entriesAndFlags?.0 ?? [] - - let animated = (previousState?.selectedMessageIds == nil) != (strongSelf.searchStateValue.selectedMessageIds == nil) - let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, searchQuery: strongSelf.searchQueryValue ?? "", context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: filter, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: { - guard let strongSelf = self else { - return - } - strongSelf.updateSearchState { state in - var state = state - state.expandLocalSearch = !state.expandLocalSearch - return state - } - }, toggleExpandGlobalResults: { - guard let strongSelf = self else { - return - } - strongSelf.updateSearchState { state in - var state = state - state.expandGlobalSearch = !state.expandGlobalSearch - return state - } - }, searchPeer: { peer in - guard let strongSelf = self else { - return - } - let isGroup: Bool - if let channel = peer as? TelegramChannel, case .group = channel.info { - isGroup = true - } else if peer.id.namespace == Namespaces.Peer.CloudGroup { - isGroup = true + if let (layout, _) = strongSelf.validLayout { + let filters: [ChatListSearchFilter] + if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { + filters = suggestedFilters } else { - isGroup = false + filters = [.chats, .media, .links, .files, .music, .voice] } - strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedPeer((peer.id, isGroup, peer.compactDisplayTitle)), clearQuery: true) - strongSelf.dismissInput?() - }, searchResults: newEntries.compactMap { entry -> Message? in - if case let .message(message, _, _, _, _, _, _) = entry { - return message - } else { - return nil - } - }, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in - guard let strongSelf = self else { - return - } - strongSelf.messageContextAction(message, node: node, rect: rect, gesture: gesture) - }) - strongSelf.enqueueTransition(transition, firstTime: firstTime) - - var messages: [Message] = [] - for entry in newEntries { - if case let .message(message, _, _, _, _, _, _) = entry { - messages.append(message) - } - } - strongSelf.searchCurrentMessages = messages - - strongSelf.suggestedPeersValue = Array(peers.prefix(8)) - } - })) - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - strongSelf.presentationData = presentationData - strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations))) - - if previousTheme !== presentationData.theme { - strongSelf.updateTheme(theme: presentationData.theme) + strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) } } - }) - - self.recentListNode.beganInteractiveDragging = { [weak self] in - self?.dismissInput?() - } - - self.listNode.beganInteractiveDragging = { [weak self] in - self?.dismissInput?() - } - - self.mediaNode.beganInteractiveDragging = { [weak self] in - self?.dismissInput?() - } - - self.listNode.visibleBottomContentOffsetChanged = { offset in - guard case let .known(value) = offset, value < 160.0 else { - return - } - loadMore() - } - - self.mediaNode.loadMore = { - loadMore() } self.filterContainerNode.filterPressed = { [weak self] filter in guard let strongSelf = self else { return } - var messageTags = strongSelf.currentSearchOptions.messageTags + + var key: ChatListSearchPaneKey? var maxDate = strongSelf.currentSearchOptions.maxDate var peer = strongSelf.currentSearchOptions.peer - var clearQuery: Bool = false + switch filter { + case .chats: + key = .chats case .media: - messageTags = .photoOrVideo + key = .media case .links: - messageTags = .webPage + key = .links case .files: - messageTags = .file + key = .files case .music: - messageTags = .music + key = .music case .voice: - messageTags = .voiceOrInstantVideo + key = .voice case let .date(date, title): maxDate = (date, title) - clearQuery = true case let .peer(id, isGroup, _, compactDisplayTitle): peer = (id, isGroup, compactDisplayTitle) - clearQuery = true } - strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMessageTags(messageTags).withUpdatedMaxDate(maxDate).withUpdatedPeer(peer), clearQuery: clearQuery) + + if let key = key { + strongSelf.paneContainerNode.requestSelectPane(key) + } else { + strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMaxDate(maxDate).withUpdatedPeer(peer), clearQuery: true) + } } self.suggestedFiltersDisposable.set((combineLatest(self.suggestedPeers.get(), self.suggestedDates.get()) @@ -1711,94 +342,40 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filteredFilters.append(filter) } } - let previousFilters = strongSelf.suggestedFilters ?? [] + + let previousFilters = strongSelf.suggestedFilters strongSelf.suggestedFilters = filteredFilters - if previousFilters.isEmpty != filteredFilters.isEmpty { - strongSelf.updatedDisplayFiltersPanel?(strongSelf.searchOptionsValue?.messageTags == nil || strongSelf.hasSuggestions) + if filteredFilters != previousFilters { if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } })) - self.mediaStatusDisposable = (combineLatest(context.sharedContext.mediaManager.globalMediaPlayerState, self.searchOptions.get()) - |> mapToSignal { playlistStateAndType, searchOptions -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in - if let (account, state, type) = playlistStateAndType { - switch state { - case let .state(state): - if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .custom = playlistId { - if case .music = type, searchOptions?.messageTags == .music { - return .single((account, state, type)) - } else if case .voice = type, searchOptions?.messageTags == .voiceOrInstantVideo { - return .single((account, state, type)) - } else { - return .single(nil) |> delay(0.1, queue: .mainQueue()) - } - } else { - return .single(nil) |> delay(0.1, queue: .mainQueue()) - } - case .loading: - return .single(nil) |> delay(0.1, queue: .mainQueue()) - } - } else { - return .single(nil) - } - } - |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in - guard let strongSelf = self else { - return - } - if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) || - !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) || - !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) || - strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 { - - if let playlistStateAndType = playlistStateAndType { - strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0) - } else { - strongSelf.playlistStateAndType = nil - } - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) - } - } - }) + self._ready.set(self.paneContainerNode.isReady.get() + |> map { _ in Void() }) } deinit { self.activeActionDisposable.dispose() - self.updatedRecentPeersDisposable.dispose() - self.recentDisposable.dispose() self.searchDisposable.dispose() self.presentationDataDisposable?.dispose() - self.mediaStatusDisposable?.dispose() - self.playlistPreloadDisposable?.dispose() self.suggestedFiltersDisposable.dispose() } - override public func didLoad() { - super.didLoad() - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - - self.emptyResultsAnimationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:)))) - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancel?() - } - } - - @objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state, !self.emptyResultsAnimationNode.isPlaying { - let _ = self.emptyResultsAnimationNode.playIfNeeded() + private func updateState(_ f: (ChatListSearchContainerNodeSearchState) -> ChatListSearchContainerNodeSearchState) { + let state = f(self.stateValue) + if state != self.stateValue { + self.stateValue = state + self.statePromise.set(state) } + self.paneContainerNode.currentPane?.node.updateSelectedMessages(animated: true) + self.selectionPanelNode?.selectedMessages = self.stateValue.selectedMessageIds ?? [] } private var currentSearchOptions: ChatListSearchOptions { - return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, minDate: nil, maxDate: nil, messageTags: nil) + return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, minDate: nil, maxDate: nil) } public override func searchTokensUpdated(tokens: [SearchBarToken]) { @@ -1807,9 +384,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo for token in tokens { tokensIdSet.insert(token.id) } - if !tokensIdSet.contains(ChatListTokenId.filter.rawValue) && updatedOptions?.messageTags != nil { - updatedOptions = updatedOptions?.withUpdatedMessageTags(nil) - } if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.maxDate != nil { updatedOptions = updatedOptions?.withUpdatedMaxDate(nil) } @@ -1828,31 +402,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.searchOptions.set(.single(options)) var tokens: [SearchBarToken] = [] - if let messageTags = options?.messageTags { - var title: String? - var icon: UIImage? - if messageTags == .photoOrVideo { - title = self.presentationData.strings.ChatList_Search_FilterMedia - icon = UIImage(bundleImageName: "Chat List/Search/Media") - } else if messageTags == .webPage { - title = self.presentationData.strings.ChatList_Search_FilterLinks - icon = UIImage(bundleImageName: "Chat List/Search/Links") - } else if messageTags == .file { - title = self.presentationData.strings.ChatList_Search_FilterFiles - icon = UIImage(bundleImageName: "Chat List/Search/Files") - } else if messageTags == .music { - title = self.presentationData.strings.ChatList_Search_FilterMusic - icon = UIImage(bundleImageName: "Chat List/Search/Music") - } else if messageTags == .voiceOrInstantVideo { - title = self.presentationData.strings.ChatList_Search_FilterVoice - icon = UIImage(bundleImageName: "Chat List/Search/Voice") - } - - if let title = title { - tokens.append(SearchBarToken(id: ChatListTokenId.filter.rawValue, icon: icon, title: title)) - } - } - if let (peerId, isGroup, peerName) = options?.peer { let image: UIImage? if isGroup { @@ -1868,7 +417,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let (_, dateTitle) = options?.maxDate { tokens.append(SearchBarToken(id: ChatListTokenId.date.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Calendar"), title: dateTitle)) - self.suggestedDatesValue = [] + self.suggestedDates.set(.single([])) } if clearQuery { @@ -1876,415 +425,59 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } else { self.setQuery?(nil, tokens, self.searchQueryValue ?? "") } - - self.updatedDisplayFiltersPanel?(options?.messageTags == nil || self.hasSuggestions) } - private func updateTheme(theme: PresentationTheme) { - self.backgroundColor = self.peersFilter.contains(.excludeRecent) ? nil : theme.chatList.backgroundColor - self.dimNode.backgroundColor = self.peersFilter.contains(.excludeRecent) ? UIColor.black.withAlphaComponent(0.5) : theme.chatList.backgroundColor - self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor - self.listNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor - - self.listNode.forEachItemHeaderNode({ itemHeaderNode in - if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { - itemHeaderNode.updateTheme(theme: theme) - } else if let itemHeaderNode = itemHeaderNode as? ListMessageDateHeaderNode { - itemHeaderNode.updateThemeAndStrings(theme: theme, strings: self.presentationData.strings) - } - }) - self.recentListNode.forEachItemHeaderNode({ itemHeaderNode in - if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { - itemHeaderNode.updateTheme(theme: theme) - } - }) - } - - private func updateState(_ f: (ChatListSearchContainerNodeState) -> ChatListSearchContainerNodeState) { - let state = f(self.stateValue) - if state != self.stateValue { - self.stateValue = state - self.statePromise.set(state) - } - } - - private func updateSearchState(_ f: (ChatListSearchContainerNodeSearchState) -> ChatListSearchContainerNodeSearchState) { - let state = f(self.searchStateValue) - if state != self.searchStateValue { - self.searchStateValue = state - self.searchStatePromise.set(state) - } - self.mediaNode.selectedMessageIds = self.searchStateValue.selectedMessageIds - self.mediaNode.updateSelectedMessages(animated: true) - self.selectionPanelNode?.selectedMessages = self.searchStateValue.selectedMessageIds ?? [] - } +// private func updateTheme(theme: PresentationTheme) { +// self.backgroundColor = self.peersFilter.contains(.excludeRecent) ? nil : theme.chatList.backgroundColor +// self.dimNode.backgroundColor = self.peersFilter.contains(.excludeRecent) ? UIColor.black.withAlphaComponent(0.5) : theme.chatList.backgroundColor +// self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor +// self.listNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor +// +// self.listNode.forEachItemHeaderNode({ itemHeaderNode in +// if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { +// itemHeaderNode.updateTheme(theme: theme) +// } else if let itemHeaderNode = itemHeaderNode as? ListMessageDateHeaderNode { +// itemHeaderNode.updateThemeAndStrings(theme: theme, strings: self.presentationData.strings) +// } +// }) +// self.recentListNode.forEachItemHeaderNode({ itemHeaderNode in +// if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { +// itemHeaderNode.updateTheme(theme: theme) +// } +// }) +// } override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil - self.interaction?.searchTextHighightState = searchQuery +// self.interaction?.searchTextHighightState = searchQuery self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery - self.suggestedDatesValue = suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat))) } - - private var hasSuggestions: Bool { - if let suggestedFilters = self.suggestedFilters { - return !suggestedFilters.isEmpty - } else { - return false - } - } - - private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) { - self.enqueuedRecentTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - } - } - - private func dequeueRecentTransition() { - if let (transition, firstTime) = self.enqueuedRecentTransitions.first { - self.enqueuedRecentTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.PreferSynchronousDrawing) - } else { - options.insert(.AnimateInsertion) - } - - self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - } - - private func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - options.insert(.PreferSynchronousDrawing) - options.insert(.PreferSynchronousResourceLoading) - - if transition.animated { - options.insert(.AnimateInsertion) - } - - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - if let strongSelf = self { - let searchOptions = strongSelf.searchOptionsValue - strongSelf.listNode.isHidden = searchOptions?.messageTags == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty - strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden - - let displayingResults = transition.displayingResults - if !displayingResults { - strongSelf.listNode.isHidden = true - strongSelf.mediaNode.isHidden = true - } - - let emptyResults = displayingResults && transition.isEmpty - if emptyResults { - let emptyResultsTitle: String - let emptyResultsText: String - if !transition.query.isEmpty { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0 - } else { - if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peer == nil { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter - if searchOptions.messageTags == .photoOrVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia - } else if searchOptions.messageTags == .webPage { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks - } else if searchOptions.messageTags == .file { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles - } else if searchOptions.messageTags == .music { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic - } else if searchOptions.messageTags == .voiceOrInstantVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice - } else { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription - } - } else { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription - } - } - - strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - } - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults - strongSelf.emptyResultsTitleNode.isHidden = !emptyResults - strongSelf.emptyResultsTextNode.isHidden = !emptyResults - strongSelf.emptyResultsAnimationNode.visibility = emptyResults - - if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.messageTags != .photoOrVideo, transition.query.isEmpty { - if searchOptions.messageTags == .webPage { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Links") - } else if searchOptions.messageTags == .file { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Files") - } else if searchOptions.messageTags == .music || searchOptions.messageTags == .voiceOrInstantVideo { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Music") - } - - strongSelf.loadingNode.isHidden = !transition.isLoading - } else { - strongSelf.loadingNode.isHidden = true - } - strongSelf.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent) - strongSelf.dimNode.isHidden = displayingResults - strongSelf.backgroundColor = !displayingResults && strongSelf.peersFilter.contains(.excludeRecent) ? nil : strongSelf.presentationData.theme.chatList.backgroundColor - } - }) - } - } - + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - let hadValidLayout = self.validLayout != nil self.validLayout = (layout, navigationBarHeight) var topInset = navigationBarHeight - var topPanelHeight: CGFloat = 0.0 - if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType { - let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - topPanelHeight = panelHeight - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) - if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { - transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) - mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) - switch order { - case .regular: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) - case .reversed: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) - case .random: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) - } - let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState - |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in - guard let value = value else { - return .single(nil) - } - switch value.1 { - case .state: - return .single(value) - case .loading: - return .single(value) |> delay(0.1, queue: .mainQueue()) - } - } - - mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus - |> map { state -> MediaPlayerStatus in - if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { - return state.status - } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) - } - } - } else { - if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { - self.mediaAccessoryPanel = nil - self.dismissingPanel = mediaAccessoryPanel - mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in - mediaAccessoryPanel?.removeFromSupernode() - if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { - strongSelf.dismissingPanel = nil - } - }) - } - - let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context) - mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo - mediaAccessoryPanel.close = { [weak self] in - if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { - strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) - } - } - mediaAccessoryPanel.toggleRate = { - [weak self] in - guard let strongSelf = self else { - return - } - let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in - let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings) as? MusicPlaybackSettings ?? MusicPlaybackSettings.defaultSettings - - let nextRate: AudioPlaybackRate - switch settings.voicePlaybackRate { - case .x1: - nextRate = .x2 - case .x2: - nextRate = .x1 - } - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in - return settings.withUpdatedVoicePlaybackRate(nextRate) - }) - return nextRate - } - |> deliverOnMainQueue).start(next: { baseRate in - guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else { - return - } - strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type) - }) - } - mediaAccessoryPanel.togglePlayPause = { [weak self] in - if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { - strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) - } - } - mediaAccessoryPanel.playPrevious = { [weak self] in - if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { - strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type) - } - } - mediaAccessoryPanel.playNext = { [weak self] in - if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { - strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type) - } - } - mediaAccessoryPanel.tapAction = { [weak self] in - guard let strongSelf = self, let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else { - return - } - if let id = state.id as? PeerMessagesMediaPlaylistItemId { - if type == .music { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60), id: 0), context: strongSelf.context, chatLocation: .peer(id.messageId.peerId), chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) - - var cancelImpl: (() -> Void)? - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - self?.interaction?.present(controller) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = MetaDisposable() - var progressStarted = false - strongSelf.playlistPreloadDisposable?.dispose() - - - strongSelf.playlistPreloadDisposable = (signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - |> deliverOnMainQueue).start(next: { index in - guard let strongSelf = self else { - return - } - if let _ = index.0 { - let controllerContext: AccountContext - if account.id == strongSelf.context.account.id { - controllerContext = strongSelf.context - } else { - controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) - } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, isGlobalSearch: true, parentNavigationController: strongSelf.navigationController) - strongSelf.dismissInput() - strongSelf.interaction?.present(controller) - } else if index.1 { - if !progressStarted { - progressStarted = true - progressDisposable.set(progressSignal.start()) - } - } - }, completed: { - }) - cancelImpl = { - self?.playlistPreloadDisposable?.dispose() - } - } else { - strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) - } - } - } - mediaAccessoryPanel.frame = panelFrame - if let dismissingPanel = self.dismissingPanel { - self.mediaAccessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) - } else { - self.mediaAccessoryPanelContainer.addSubnode(mediaAccessoryPanel) - } - self.mediaAccessoryPanel = (mediaAccessoryPanel, type) - mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate) - switch order { - case .regular: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) - case .reversed: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) - case .random: - mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) - } - mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState - |> map { state -> MediaPlayerStatus in - if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { - return state.status - } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) - } - } - mediaAccessoryPanel.animateIn(transition: transition) - } - } else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { - self.mediaAccessoryPanel = nil - self.dismissingPanel = mediaAccessoryPanel - mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in - mediaAccessoryPanel?.removeFromSupernode() - if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { - strongSelf.dismissingPanel = nil - } - }) - } - transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight))) topInset += topPanelHeight - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - - transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 37.0))) - - self.loadingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: 422.0)) + transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 38.0))) let filters: [ChatListSearchFilter] if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { - filters = [.media, .links, .files, .music, .voice] + filters = [.chats, .media, .links, .files, .music, .voice] } - self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 37.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) - if let selectedMessageIds = self.searchStateValue.selectedMessageIds { + if let selectedMessageIds = self.stateValue.selectedMessageIds { var wasAdded = false let selectionPanelNode: ChatListSearchMessageSelectionPanelNode if let current = self.selectionPanelNode { @@ -2297,7 +490,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } strongSelf.deleteMessages(messageIds: nil) }, shareMessages: { [weak self] in - guard let strongSelf = self, let messageIds = strongSelf.searchStateValue.selectedMessageIds, !messageIds.isEmpty else { + guard let strongSelf = self, let messageIds = strongSelf.stateValue.selectedMessageIds, !messageIds.isEmpty else { return } let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in @@ -2328,18 +521,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo guard let strongSelf = self else { return .complete() } - - var peers: [PeerId: Peer] = [:] - var messages: [MessageId: Message] = [:] - if let currentMessages = strongSelf.searchCurrentMessages { - for message in currentMessages { - messages[message.id] = message - for (_, peer) in message.peers { - peers[peer.id] = peer - } - } - } - + + let (peers, messages) = strongSelf.currentMessages return strongSelf.context.sharedContext.chatAvailableMessageActions(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, messages: messages, peers: peers) } self.selectionPanelNode = selectionPanelNode @@ -2361,184 +544,36 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) } - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + transition.updateFrame(node: self.paneContainerNode, frame: CGRect(x: 0.0, y: topInset - 48.0, width: layout.size.width, height: layout.size.height - topInset + 48)) - self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - self.mediaNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)) - self.mediaNode.update(size: layout.size, sideInset: layout.safeInsets.left, bottomInset: layout.insets(options: [.input]).bottom, visibleHeight: layout.size.height - navigationBarHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition) - - let padding: CGFloat = 16.0 - let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - let insets = layout.insets(options: [.input]) - var emptyAnimationHeight = self.animationSize.height - var emptyAnimationSpacing: CGFloat = 8.0 - if case .landscape = layout.orientation, case .compact = layout.metrics.widthClass { - emptyAnimationHeight = 0.0 - emptyAnimationSpacing = 0.0 - } - let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing - let emptyAnimationY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) - - let textTransition = ContainedViewLayoutTransition.immediate - textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) - textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) - textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) - self.emptyResultsAnimationNode.updateLayout(size: self.animationSize) - - if !hadValidLayout { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() + self.paneContainerNode.update(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), sideInset: layout.safeInsets.left, bottomInset: layout.inputHeight ?? 0.0, visibleHeight: layout.size.height - topInset, presentationData: self.presentationData, transition: transition) + } + + private var currentMessages: ([PeerId: Peer], [MessageId: Message]) { + var peers: [PeerId: Peer] = [:] + let messages: [MessageId: Message] = self.paneContainerNode.allCurrentMessages() + for (_, message) in messages { + for (_, peer) in message.peers { + peers[peer.id] = peer } } + return (peers, messages) } override public func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? { - var selectedItemNode: ASDisplayNode? - var bounds: CGRect - if !self.recentListNode.isHidden { - let adjustedLocation = self.convert(location, to: self.recentListNode) - self.recentListNode.forEachItemNode { itemNode in - if itemNode.frame.contains(adjustedLocation) { - selectedItemNode = itemNode - } - } + if let node = self.paneContainerNode.currentPane?.node { + let adjustedLocation = self.convert(location, to: node) + return self.paneContainerNode.currentPane?.node.previewViewAndActionAtLocation(adjustedLocation) } else { - let adjustedLocation = self.convert(location, to: self.listNode) - self.listNode.forEachItemNode { itemNode in - if itemNode.frame.contains(adjustedLocation) { - selectedItemNode = itemNode - } - } + return nil } - if let selectedItemNode = selectedItemNode as? ChatListRecentPeersListItemNode { - if let result = selectedItemNode.viewAndPeerAtPoint(self.convert(location, to: selectedItemNode)) { - return (result.0, result.0.bounds, result.1) - } - } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.chatPeer { - if selectedItemNode.frame.height > 50.0 { - bounds = CGRect(x: 0.0, y: selectedItemNode.frame.height - 50.0, width: selectedItemNode.frame.width, height: 50.0) - } else { - bounds = selectedItemNode.bounds - } - return (selectedItemNode.view, bounds, peer.id) - } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { - if selectedItemNode.frame.height > 76.0 { - bounds = CGRect(x: 0.0, y: selectedItemNode.frame.height - 76.0, width: selectedItemNode.frame.width, height: 76.0) - } else { - bounds = selectedItemNode.bounds - } - switch item.content { - case let .peer(messages, peer, _, _, _, _, _, _, _, _, _, _): - return (selectedItemNode.view, bounds, messages.last?.id ?? peer.peerId) - case let .groupReference(groupId, _, _, _, _): - return (selectedItemNode.view, bounds, groupId) - } - } - return nil - } - - private func clearRecentSearch() { - let presentationData = self.presentationData - let actionSheet = ActionSheetController(presentationData: presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.WebSearch_RecentSectionClear, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - let _ = (clearRecentlySearchedPeers(postbox: strongSelf.context.account.postbox) - |> deliverOnMainQueue).start() - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.dismissInput() - self.interaction?.present(actionSheet) } override public func scrollToTop() { - if !self.listNode.isHidden { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } else { - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } + let _ = self.paneContainerNode.scrollToTop() } - func messageContextActions(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { - let gesture: ContextGesture? = anyRecognizer as? ContextGesture - let _ = (chatMediaListPreviewControllerData(context: self.context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic(value: nil), message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.navigationController) - |> deliverOnMainQueue).start(next: { [weak self] previewData in - guard let strongSelf = self else { - gesture?.cancel() - return - } - if let previewData = previewData { - let context = strongSelf.context - let strings = strongSelf.presentationData.strings -// let items = chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) -// |> map { actions -> [ContextMenuItem] in - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in - c.dismiss(completion: { - self?.openMessage(message.peers[message.id.peerId]!, message.id) - }) - }))) - - items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in - c.dismiss(completion: { - if let strongSelf = self { - strongSelf.forwardMessages(messageIds: [message.id]) - } - }) - }))) - - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuMore, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - if let strongSelf = self { - strongSelf.dismissInput() - - strongSelf.updateSearchState { state in - return state.withUpdatedSelectedMessageIds([message.id]) - } - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - - f(.default) - }))) - - switch previewData { - case let .gallery(gallery): - gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) - strongSelf.presentInGlobalOverlay?(contextController, nil) - case .instantPage: - break - } - } - }) - } - - func messageContextAction(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { + private func messageContextAction(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { guard let node = node as? ContextExtractedContentContainingNode else { return } @@ -2558,59 +593,132 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } let gesture: ContextGesture? = anyRecognizer as? ContextGesture - var items: [ContextMenuItem] = [] - if let linkForCopying = linkForCopying { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - c.dismiss(completion: {}) - UIPasteboard.general.string = linkForCopying + let (peers, messages) = self.currentMessages + let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers) + |> map { actions -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + + + if let linkForCopying = linkForCopying { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + c.dismiss(completion: {}) + UIPasteboard.general.string = linkForCopying + }))) + } + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { [weak self] in + if let strongSelf = self { + strongSelf.forwardMessages(messageIds: Set([message.id])) + } + }) }))) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c.dismiss(completion: { [weak self] in + self?.openMessage(message.peers[message.id.peerId]!, message.id) + }) + }))) + + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuMore, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), 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) + } + } + }) + }))) + return items } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { [weak self] in - if let strongSelf = self { - strongSelf.forwardMessages(messageIds: Set([message.id])) - } - }) - }))) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { [weak self] in - self?.openMessage(message.peers[message.id.peerId]!, message.id) - }) - }))) - - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuMore, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c.dismiss(completion: { - if let strongSelf = self { - strongSelf.dismissInput() - - strongSelf.updateSearchState { state in - return state.withUpdatedSelectedMessageIds([message.id]) - } - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - }) - }))) - - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items, reactionItems: [], recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) } + private func mediaMessageContextAction(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { + let gesture: ContextGesture? = anyRecognizer as? ContextGesture + let _ = (chatMediaListPreviewControllerData(context: self.context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic(value: nil), message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: self.navigationController) + |> deliverOnMainQueue).start(next: { [weak self] previewData in + guard let strongSelf = self else { + gesture?.cancel() + return + } + if let previewData = previewData { + let context = strongSelf.context + let strings = strongSelf.presentationData.strings + + let (peers, messages) = strongSelf.currentMessages + let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers) + |> map { actions -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in + c.dismiss(completion: { + self?.openMessage(message.peers[message.id.peerId]!, message.id) + }) + }))) + + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in + c.dismiss(completion: { + if let strongSelf = self { + strongSelf.forwardMessages(messageIds: [message.id]) + } + }) + }))) + + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuMore, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + 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: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + f(.default) + }))) + + return items + } + + switch previewData { + case let .gallery(gallery): + gallery.setHintWillBePresentedInPreviewingContext(true) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) + strongSelf.presentInGlobalOverlay?(contextController, nil) + case .instantPage: + break + } + } + }) + } + public override func searchTextClearTokens() { self.updateSearchOptions(nil) self.setQuery?(nil, [], self.searchQueryValue ?? "") } + func deleteMessages(messageIds: Set?) { - let messageIds = messageIds ?? self.searchStateValue.selectedMessageIds + let messageIds = messageIds ?? self.stateValue.selectedMessageIds } func forwardMessages(messageIds: Set?) { - let messageIds = messageIds ?? self.searchStateValue.selectedMessageIds + let messageIds = messageIds ?? self.stateValue.selectedMessageIds if let messageIds = messageIds, !messageIds.isEmpty { let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled])) peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in @@ -2647,8 +755,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let peerSelectionController = peerSelectionController { peerSelectionController.dismiss() } - - strongSelf.updateSearchState { state in + + strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) } if let (layout, navigationBarHeight) = strongSelf.validLayout { @@ -2673,8 +781,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo peerSelectionController.dismiss() } }) - - strongSelf.updateSearchState { state in + + strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) } if let (layout, navigationBarHeight) = strongSelf.validLayout { diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index de29892d35..180733dcd3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -8,6 +8,7 @@ import TelegramCore import TelegramPresentationData enum ChatListSearchFilter: Equatable { + case chats case media case links case files @@ -18,16 +19,18 @@ enum ChatListSearchFilter: Equatable { var id: Int32 { switch self { - case .media: + case .chats: return 0 - case .links: + case .media: return 1 - case .files: + case .links: return 2 - case .music: + case .files: return 3 - case .voice: + case .music: return 4 + case .voice: + return 5 case let .peer(peerId, _, _, _): return peerId.id case let .date(date, _): @@ -41,12 +44,10 @@ private final class ItemNode: ASDisplayNode { private let iconNode: ASImageNode private let titleNode: ImmediateTextNode + private let titleActiveNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode private var selectionFraction: CGFloat = 0.0 - private(set) var unreadCount: Int = 0 - - private var isReordering: Bool = false private var theme: PresentationTheme? @@ -63,11 +64,17 @@ private final class ItemNode: ASDisplayNode { self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.titleActiveNode = ImmediateTextNode() + self.titleActiveNode.displaysAsynchronously = false + self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.titleActiveNode.alpha = 0.0 + self.buttonNode = HighlightTrackingButtonNode() super.init() self.addSubnode(self.titleNode) + self.addSubnode(self.titleActiveNode) self.addSubnode(self.iconNode) self.addSubnode(self.buttonNode) @@ -95,27 +102,32 @@ private final class ItemNode: ASDisplayNode { self.pressed() } - func update(type: ChatListSearchFilter, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func update(type: ChatListSearchFilter, presentationData: PresentationData, selectionFraction: CGFloat, transition: ContainedViewLayoutTransition) { + self.selectionFraction = selectionFraction + let title: String let icon: UIImage? let color = presentationData.theme.list.itemSecondaryTextColor switch type { + case .chats: + title = presentationData.strings.ChatList_Search_FilterChats + icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia - icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Media"), color: color) + icon = nil case .links: title = presentationData.strings.ChatList_Search_FilterLinks - icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Links"), color: color) + icon = nil case .files: title = presentationData.strings.ChatList_Search_FilterFiles - icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Files"), color: color) + icon = nil case .music: title = presentationData.strings.ChatList_Search_FilterMusic - icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Music"), color: color) + icon = nil case .voice: title = presentationData.strings.ChatList_Search_FilterVoice - icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Voice"), color: color) + icon = nil case let .peer(peerId, isGroup, displayTitle, _): title = displayTitle let image: UIImage? @@ -133,6 +145,12 @@ private final class ItemNode: ASDisplayNode { } self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: color) + self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + + let selectionAlpha: CGFloat = selectionFraction * selectionFraction + let deselectionAlpha: CGFloat = 1.0// - selectionFraction + transition.updateAlpha(node: self.titleNode, alpha: deselectionAlpha) + transition.updateAlpha(node: self.titleActiveNode, alpha: selectionAlpha) if self.theme !== presentationData.theme { self.theme = presentationData.theme @@ -141,14 +159,17 @@ private final class ItemNode: ASDisplayNode { } func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let iconInset: CGFloat = 22.0 + var iconInset: CGFloat = 0.0 if let image = self.iconNode.image { + iconInset = 22.0 self.iconNode.frame = CGRect(x: 0.0, y: floorToScreenPixels((height - image.size.height) / 2.0), width: image.size.width, height: image.size.height) } let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) + let _ = self.titleActiveNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame + self.titleActiveNode.frame = titleFrame return titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset } @@ -177,15 +198,23 @@ enum ChatListSearchFilterEntry: Equatable { final class ChatListSearchFiltersContainerNode: ASDisplayNode { private let scrollNode: ASScrollNode + private let selectedLineNode: ASImageNode private var itemNodes: [ChatListSearchFilterEntryId: ItemNode] = [:] var filterPressed: ((ChatListSearchFilter) -> Void)? - private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], presentationData: PresentationData)? + private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData)? + private var previousSelectedAbsFrame: CGRect? + private var previousSelectedFrame: CGRect? + override init() { self.scrollNode = ASScrollNode() + self.selectedLineNode = ASImageNode() + self.selectedLineNode.displaysAsynchronously = false + self.selectedLineNode.displayWithoutProcessing = true + super.init() self.scrollNode.view.showsHorizontalScrollIndicator = false @@ -197,21 +226,31 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { } self.addSubnode(self.scrollNode) + self.addSubnode(self.selectedLineNode) } func cancelAnimations() { self.scrollNode.layer.removeAllAnimations() } - func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) { + func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) { let isFirstTime = self.currentParams == nil let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition + var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter + let previousScrollBounds = self.scrollNode.bounds + let previousContentWidth = self.scrollNode.view.contentSize.width + if self.currentParams?.presentationData.theme !== presentationData.theme { self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.selectedLineNode.image = generateImage(CGSize(width: 8.0, height: 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + })?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1) } - self.currentParams = (size: size, sideInset: sideInset, filters: filters, presentationData: presentationData) + self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, transitionFraction: transitionFraction, presentationData: presentationData) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -229,7 +268,19 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { }) self.itemNodes[filter.id] = itemNode } - itemNode.update(type: type, presentationData: presentationData, transition: itemNodeTransition) + + let selectionFraction: CGFloat + if selectedFilter == filter.id { + selectionFraction = 1.0 - abs(transitionFraction) + } else if i != 0 && selectedFilter == filters[i - 1].id { + selectionFraction = max(0.0, -transitionFraction) + } else if i != filters.count - 1 && selectedFilter == filters[i + 1].id { + selectionFraction = max(0.0, transitionFraction) + } else { + selectionFraction = 0.0 + } + + itemNode.update(type: type, presentationData: presentationData, selectionFraction: selectionFraction, transition: itemNodeTransition) } } @@ -253,6 +304,7 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { var tabSizes: [(ChatListSearchFilterEntryId, CGSize, ItemNode, Bool)] = [] var totalRawTabSize: CGFloat = 0.0 + var selectionFrames: [CGRect] = [] for filter in filters { guard let itemNode = self.itemNodes[filter.id] else { @@ -308,6 +360,8 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { paneNode.updateArea(size: paneFrame.size, sideInset: spacing / 2.0, transition: itemNodeTransition) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing / 2.0, bottom: 0.0, right: -spacing / 2.0) + selectionFrames.append(paneFrame) + leftOffset += paneNodeSize.width + spacing } leftOffset -= spacing @@ -315,6 +369,66 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height) + var selectedFrame: CGRect? + if let selectedFilter = selectedFilter, let currentIndex = filters.firstIndex(where: { $0.id == selectedFilter }) { + func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { + return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) + } + + if currentIndex != 0 && transitionFraction > 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex - 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else if currentIndex != filters.count - 1 && transitionFraction < 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex + 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else { + selectedFrame = selectionFrames[currentIndex] + } + } + + if let selectedFrame = selectedFrame { + let wasAdded = self.selectedLineNode.isHidden + self.selectedLineNode.isHidden = false + let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 4.0), size: CGSize(width: selectedFrame.width, height: 4.0)) + if wasAdded { + self.selectedLineNode.frame = lineFrame + self.selectedLineNode.alpha = 0.0 + } else { + transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) + } + transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0) + + if let previousSelectedFrame = self.previousSelectedFrame { + let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) + if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 { + focusOnSelectedFilter = true + } + } + + if focusOnSelectedFilter { + let updatedBounds: CGRect + if transitionFraction.isZero && selectedFilter == filters.first?.id { + updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + } else if transitionFraction.isZero && selectedFilter == filters.last?.id { + updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size) + } else { + let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) + updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size) + } + self.scrollNode.bounds = updatedBounds + } + transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + + self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) + self.previousSelectedFrame = selectedFrame + } else { + self.selectedLineNode.isHidden = true + self.previousSelectedAbsFrame = nil + self.previousSelectedFrame = nil + } + if updated && self.scrollNode.view.contentOffset.x > 0.0 { self.scrollNode.view.contentOffset = CGPoint() } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift new file mode 100644 index 0000000000..8115f85b98 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -0,0 +1,2042 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import PresentationDataUtils +import AccountContext +import MergeLists +import ItemListUI +import ContextUI +import ContactListUI +import ContactsPeerItem +import PhotoResources +import TelegramUIPreferences +import UniversalMediaPlayer +import TelegramBaseController +import OverlayStatusController +import ListMessageItem +import AnimatedStickerNode +import ChatListSearchItemHeader +import PhoneNumberFormat +import InstantPageUI +import GalleryData +import AppBundle + +private enum ChatListRecentEntryStableId: Hashable { + case topPeers + case peerId(PeerId) +} + +private enum ChatListRecentEntry: Comparable, Identifiable { + case topPeers([Peer], PresentationTheme, PresentationStrings) + case peer(index: Int, peer: RecentlySearchedPeer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder) + + var stableId: ChatListRecentEntryStableId { + switch self { + case .topPeers: + return .topPeers + case let .peer(_, peer, _, _, _, _, _): + return .peerId(peer.peer.peerId) + } + } + + static func ==(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool { + switch lhs { + case let .topPeers(lhsPeers, lhsTheme, lhsStrings): + if case let .topPeers(rhsPeers, rhsTheme, rhsStrings) = rhs { + if lhsPeers.count != rhsPeers.count { + return false + } + for i in 0 ..< lhsPeers.count { + if !lhsPeers[i].isEqual(rhsPeers[i]) { + return false + } + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + } else { + return false + } + case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder): + if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsTimeFormat == rhsTimeFormat && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder { + return true + } else { + return false + } + } + } + + static func <(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool { + switch lhs { + case .topPeers: + return true + case let .peer(lhsIndex, _, _, _, _, _, _): + switch rhs { + case .topPeers: + return false + case let .peer(rhsIndex, _, _, _, _, _, _): + return lhsIndex <= rhsIndex + } + } + } + + func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { + switch self { + case let .topPeers(peers, theme, strings): + return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in + peerSelected(peer) + }, peerContextAction: { peer, node, gesture in + if let peerContextAction = peerContextAction { + peerContextAction(peer, .recentPeers, node, gesture) + } else { + gesture?.cancel() + } + }) + case let .peer(_, peer, theme, strings, timeFormat, nameSortOrder, nameDisplayOrder): + let primaryPeer: Peer + var chatPeer: Peer? + let maybeChatPeer = peer.peer.peers[peer.peer.peerId]! + if let associatedPeerId = maybeChatPeer.associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] { + primaryPeer = associatedPeer + chatPeer = maybeChatPeer + } else { + primaryPeer = maybeChatPeer + chatPeer = maybeChatPeer + } + + var enabled = true + if filter.contains(.onlyWriteable) { + if let peer = chatPeer { + enabled = canSendMessagesToPeer(peer) + } else { + enabled = canSendMessagesToPeer(primaryPeer) + } + } + if filter.contains(.onlyPrivateChats) { + if let peer = chatPeer { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let peer = chatPeer { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } + + if filter.contains(.excludeChannels) { + if let channel = primaryPeer as? TelegramChannel, case .broadcast = channel.info { + enabled = false + } + } + + let status: ContactsPeerItemStatus + if let user = primaryPeer as? TelegramUser { + let servicePeer = isServicePeer(primaryPeer) + if user.flags.contains(.isSupport) && !servicePeer { + status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false) + } else if let _ = user.botInfo { + status = .custom(string: strings.Bot_GenericBotStatus, multiline: false) + } else if user.id != context.account.peerId && !servicePeer { + let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0) + status = .presence(presence, timeFormat) + } else { + status = .none + } + } else if let group = primaryPeer as? TelegramGroup { + status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false) + } else if let channel = primaryPeer as? TelegramChannel { + if case .group = channel.info { + if let count = peer.subpeerSummary?.count { + status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false) + } else { + status = .custom(string: strings.Group_Status, multiline: false) + } + } else { + if let count = peer.subpeerSummary?.count { + status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false) + } else { + status = .custom(string: strings.Channel_Status, multiline: false) + } + } + } else { + status = .none + } + + var isMuted = false + if let notificationSettings = peer.notificationSettings { + isMuted = notificationSettings.isRemovedFromTotalUnreadCount(default: false) + } + var badge: ContactsPeerItemBadge? + if peer.unreadCount > 0 { + badge = ContactsPeerItemBadge(count: peer.unreadCount, type: isMuted ? .inactive : .active) + } + + return ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { + clearRecentlySearchedPeers() + }), action: { _ in + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + peerSelected(chatPeer) + } + }, disabledAction: { _ in + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + disabledPeerSelected(chatPeer) + } + }, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in + return { node, gesture in + if let chatPeer = peer.peer.peers[peer.peer.peerId], chatPeer.id.namespace != Namespaces.Peer.SecretChat { + peerContextAction(chatPeer, .recentSearch, node, gesture) + } else { + gesture?.cancel() + } + } + }) + } + } +} + +public enum ChatListSearchEntryStableId: Hashable { + case localPeerId(PeerId) + case globalPeerId(PeerId) + case messageId(MessageId) + case addContact + + public static func ==(lhs: ChatListSearchEntryStableId, rhs: ChatListSearchEntryStableId) -> Bool { + switch lhs { + case let .localPeerId(peerId): + if case .localPeerId(peerId) = rhs { + return true + } else { + return false + } + case let .globalPeerId(peerId): + if case .globalPeerId(peerId) = rhs { + return true + } else { + return false + } + case let .messageId(messageId): + if case .messageId(messageId) = rhs { + return true + } else { + return false + } + case .addContact: + if case .addContact = rhs { + return true + } else { + return false + } + } + } +} + +public enum ChatListSearchSectionExpandType { + case none + case expand + case collapse +} + +public enum ChatListSearchEntry: Comparable, Identifiable { + case localPeer(Peer, Peer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) + case message(Message, RenderedPeer, CombinedPeerReadState?, ChatListPresentationData, Int32, Bool?, Bool) + case addContact(String, PresentationTheme, PresentationStrings) + + 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, _, _, _, _, _, _): + return .messageId(message.id) + 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.isEqual(rhsPeer) && arePeersEqual(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): + if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = 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 + } + 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(lhsMessage, _, _, _, _, _, _): + if case let .message(rhsMessage, _, _, _, _, _, _) = rhs { + return lhsMessage.index < rhsMessage.index + } 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, tagMask: MessageTags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem { + switch self { + case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): + let primaryPeer: Peer + var chatPeer: Peer? + 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) + } else { + enabled = false + } + } + if filter.contains(.onlyPrivateChats) { + if let peer = chatPeer { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let peer = chatPeer { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, 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 actionTitle: String? + switch expandType { + case .none: + actionTitle = nil + case .expand: + actionTitle = strings.ChatList_Search_ShowMore + case .collapse: + actionTitle = strings.ChatList_Search_ShowLess + } + header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { + toggleExpandLocalResults() + }) + } + + 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: { _ in + interaction.peerSelected(peer, nil) + }, contextAction: peerContextAction.flatMap { peerContextAction in + return { node, gesture in + if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat { + peerContextAction(chatPeer, .search, node, gesture) + } else { + gesture?.cancel() + } + } + }, arrowAction: nil) + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): + var enabled = true + if filter.contains(.onlyWriteable) { + enabled = canSendMessagesToPeer(peer.peer) + } + if filter.contains(.onlyPrivateChats) { + if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let _ = peer.peer as? TelegramGroup { + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } + + var suffixString = "" + if let subscribers = peer.subscribers, subscribers != 0 { + if peer.peer is TelegramUser { + suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + } else { + suffixString = ", \(strings.Conversation_StatusMembers(subscribers))" + } + } + + 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 actionTitle: String? + switch expandType { + case .none: + actionTitle = nil + case .expand: + actionTitle = strings.ChatList_Search_ShowMore + case .collapse: + actionTitle = strings.ChatList_Search_ShowLess + } + header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { + toggleExpandGlobalResults() + }) + } + + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + interaction.peerSelected(peer.peer, nil) + }, contextAction: peerContextAction.flatMap { peerContextAction in + return { node, gesture in + peerContextAction(peer.peer, .search, node, gesture) + } + }) + case let .message(message, peer, readState, presentationData, totalCount, selected, displayCustomHeader): + let header = ChatListSearchItemHeader(type: .messages(totalCount), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none + if let tagMask = tagMask, tagMask != .photoOrVideo { + return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true) + } else { + return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction) + } + case let .addContact(phoneNumber, theme, strings): + return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { + interaction.addContact(phoneNumber) + }) + } + } +} + +private struct ChatListSearchContainerRecentTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +public struct ChatListSearchContainerTransition { + public let deletions: [ListViewDeleteItem] + public let insertions: [ListViewInsertItem] + public let updates: [ListViewUpdateItem] + public let displayingResults: Bool + public let isEmpty: Bool + public let isLoading: Bool + public let query: String + public let animated: Bool + + public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String, animated: Bool) { + self.deletions = deletions + self.insertions = insertions + self.updates = updates + self.displayingResults = displayingResults + self.isEmpty = isEmpty + self.isLoading = isLoading + self.query = query + self.animated = animated + } +} + +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { + 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, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer), directionHint: nil) } + + 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, searchQuery: String, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: MessageTags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ChatListSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } + + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) +} + +private struct ChatListSearchListPaneNodeState: Equatable { + var expandLocalSearch: Bool = false + var expandGlobalSearch: Bool = false +} + +private func doesPeerMatchFilter(peer: Peer, filter: ChatListNodePeersFilter) -> Bool { + var enabled = true + if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { + enabled = false + } + if filter.contains(.onlyPrivateChats), !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + if filter.contains(.onlyGroups) { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } + return enabled +} + +private struct ChatListSearchMessagesResult { + let query: String + let messages: [Message] + let readStates: [PeerId: CombinedPeerReadState] + let hasMore: Bool + let totalCount: Int32 + let state: SearchMessagesState +} + +private struct ChatListSearchMessagesContext { + let result: ChatListSearchMessagesResult + let loadMoreIndex: MessageIndex? +} + +public enum ChatListSearchContextActionSource { + case recentPeers + case recentSearch + case search +} + +public struct ChatListSearchOptions { + let peer: (PeerId, Bool, String)? + let minDate: (Int32, String)? + let maxDate: (Int32, String)? + + var isEmpty: Bool { + return self.peer == nil && self.minDate == nil && self.maxDate == nil + } + + func withUpdatedPeer(_ peerIdIsGroupAndName: (PeerId, Bool, String)?) -> ChatListSearchOptions { + return ChatListSearchOptions(peer: peerIdIsGroupAndName, minDate: self.minDate, maxDate: self.maxDate) + } + + func withUpdatedMinDate(_ minDateAndTitle: (Int32, String)?) -> ChatListSearchOptions { + return ChatListSearchOptions(peer: self.peer, minDate: minDateAndTitle, maxDate: self.maxDate) + } + + func withUpdatedMaxDate(_ maxDateAndTitle: (Int32, String)?) -> ChatListSearchOptions { + return ChatListSearchOptions(peer: self.peer, minDate: self.minDate, maxDate: maxDateAndTitle) + } +} + +final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { + private let context: AccountContext + private let interaction: ChatListSearchInteraction + private let peersFilter: ChatListNodePeersFilter + private var presentationData: PresentationData + private let tagMask: MessageTags? + private let navigationController: NavigationController? + + private let recentListNode: ListView + private let loadingNode: ASImageNode + private let listNode: ListView + private let mediaNode: ChatListSearchMediaNode + private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = [] + private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] + + private var presentationDataDisposable: Disposable? + private let updatedRecentPeersDisposable = MetaDisposable() + private let recentDisposable = MetaDisposable() + + private let searchDisposable = MetaDisposable() + private let presentationDataPromise = Promise() + private var searchStateValue = ChatListSearchListPaneNodeState() + private let searchStatePromise = ValuePromise() + private let searchContextValue = Atomic(value: nil) + var searchCurrentMessages: [Message]? + + private var searchQueryValue: String? + private var searchOptionsValue: ChatListSearchOptions? + + private let _isSearching = ValuePromise(false, ignoreRepeated: true) + public var isSearching: Signal { + return self._isSearching.get() + } + + private var mediaStatusDisposable: Disposable? + private var playlistPreloadDisposable: Disposable? + + private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? + private var mediaAccessoryPanelContainer: PassthroughContainerNode + private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? + private var dismissingPanel: ASDisplayNode? + + private let emptyResultsTitleNode: ImmediateTextNode + private let emptyResultsTextNode: ImmediateTextNode + private let emptyResultsAnimationNode: AnimatedStickerNode + private var animationSize: CGSize = CGSize() + + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private let selectedMessagesPromise = Promise?>(nil) + private var selectedMessages: Set? { + didSet { + if self.selectedMessages != oldValue { + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + } + } + } + + private var hiddenMediaDisposable: Disposable? + + init(context: AccountContext, interaction: ChatListSearchInteraction, tagMask: MessageTags?, peersFilter: ChatListNodePeersFilter, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + self.context = context + self.interaction = interaction + self.peersFilter = peersFilter + self.tagMask = tagMask + self.navigationController = navigationController + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationDataPromise.set(.single(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations))) + + self.searchStatePromise.set(self.searchStateValue) + self.selectedMessages = interaction.getSelectedMessageIds() + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + + self.recentListNode = ListView() + self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor + + self.loadingNode = ASImageNode() + + self.listNode = ListView() + self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor + + var openMediaMessageImpl: ((Message, ChatControllerInteractionOpenMessageMode) -> Void)? + var transitionNodeImpl: ((MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?)? + var addToTransitionSurfaceImpl: ((UIView) -> Void)? + + self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in + openMediaMessageImpl?(message, mode) + }, messageContextAction: { message, node, rect, gesture in + interaction.mediaMessageContextAction(message, node, rect, gesture) + }, toggleMessageSelection: { messageId, selected in + interaction.toggleMessageSelection(messageId, selected) + }) + + self.mediaAccessoryPanelContainer = PassthroughContainerNode() + self.mediaAccessoryPanelContainer.clipsToBounds = true + + self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.displaysAsynchronously = false + self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) + self.emptyResultsTitleNode.textAlignment = .center + self.emptyResultsTitleNode.isHidden = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.displaysAsynchronously = false + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + self.emptyResultsAnimationNode = AnimatedStickerNode() + self.emptyResultsAnimationNode.isHidden = true + + super.init() + + if let path = getAppBundle().path(forResource: "ChatListNoResults", ofType: "tgs") { + self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + self.animationSize = CGSize(width: 124.0, height: 124.0) + } + + self.addSubnode(self.recentListNode) + self.addSubnode(self.listNode) + self.addSubnode(self.mediaNode) + self.addSubnode(self.loadingNode) + self.addSubnode(self.mediaAccessoryPanelContainer) + + self.addSubnode(self.emptyResultsAnimationNode) + self.addSubnode(self.emptyResultsTitleNode) + self.addSubnode(self.emptyResultsTextNode) + + let searchContext = Promise(nil) + let searchContextValue = self.searchContextValue + let updateSearchContext: ((ChatListSearchMessagesContext?) -> (ChatListSearchMessagesContext?, Bool)) -> Void = { f in + var shouldUpdate = false + let updated = searchContextValue.modify { current in + let (u, s) = f(current) + shouldUpdate = s + if s { + return u + } else { + return current + } + } + if shouldUpdate { + searchContext.set(.single(updated)) + } + } + + self.listNode.isHidden = true + self.mediaNode.isHidden = true + self.recentListNode.isHidden = peersFilter.contains(.excludeRecent) + + let currentRemotePeers = Atomic<([FoundPeer], [FoundPeer])?>(value: nil) + let presentationDataPromise = self.presentationDataPromise + let searchStatePromise = self.searchStatePromise + let selectionPromise = self.selectedMessagesPromise + let foundItems = combineLatest(searchQuery, searchOptions) + |> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in + if query == nil && options == nil && tagMask == nil { + let _ = currentRemotePeers.swap(nil) + return .single(nil) + } + + let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) + + let foundLocalPeers: Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> + + if let query = query { + foundLocalPeers = context.account.postbox.searchPeers(query: query.lowercased()) + |> mapToSignal { local -> Signal<([PeerView], [RenderedPeer]), NoError> in + return combineLatest(local.map { context.account.postbox.peerView(id: $0.peerId) }) |> map { views in + return (views, local) + } + } + |> mapToSignal { viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in + return context.account.postbox.unreadMessageCountsView(items: viewsAndPeers.0.map {.peer($0.peerId)}) |> map { values in + var unread: [PeerId: (Int32, Bool)] = [:] + for peerView in viewsAndPeers.0 { + var isMuted: Bool = false + if let nofiticationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + switch nofiticationSettings.muteState { + case .muted: + isMuted = true + default: + break + } + } + + let unreadCount = values.count(for: .peer(peerView.peerId)) + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peerView.peerId] = (unreadCount, isMuted) + } + } + return (peers: viewsAndPeers.1, unread: unread) + } + } + } else { + foundLocalPeers = .single((peers: [], unread: [:])) + } + + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> + let currentRemotePeersValue = currentRemotePeers.with { $0 } ?? ([], []) + if let query = query { + foundRemotePeers = ( + .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) + |> then( + searchPeers(account: context.account, query: query) + |> map { ($0.0, $0.1, false) } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + ) + } else { + foundRemotePeers = .single(([], [], false)) + } + let location: SearchMessagesLocation + if let options = options { + if let (peerId, _, _) = options.peer { + location = .peer(peerId: peerId, fromId: nil, tags: self.tagMask, topMsgId: nil, minDate: options.minDate?.0, maxDate: options.maxDate?.0) + } else { + + location = .general(tags: self.tagMask, minDate: options.minDate?.0, maxDate: options.maxDate?.0) + } + } else { + location = .general(tags: self.tagMask, minDate: nil, maxDate: nil) + } + + let finalQuery = query ?? "" + updateSearchContext { _ in + return (nil, true) + } + let foundRemoteMessages: Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> + if peersFilter.contains(.doNotSearchMessages) { + foundRemoteMessages = .single((([], [:], 0), false)) + } else { + if !finalQuery.isEmpty { + addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") + } + + let searchSignal = searchMessages(account: context.account, location: location, query: finalQuery, state: nil, limit: 50) + |> map { result, updatedState -> ChatListSearchMessagesResult in + return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + + let loadMore = searchContext.get() + |> mapToSignal { searchContext -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in + if let searchContext = searchContext, searchContext.result.hasMore { + if let _ = searchContext.loadMoreIndex { + return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80) + |> map { result, updatedState -> ChatListSearchMessagesResult in + return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + |> mapToSignal { foundMessages -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in + updateSearchContext { previous in + let updated = ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil) + return (updated, true) + } + return .complete() + } + } else { + return .single(((searchContext.result.messages, searchContext.result.readStates, searchContext.result.totalCount), false)) + } + } else { + return .complete() + } + } + + foundRemoteMessages = .single((([], [:], 0), true)) + |> then( + searchSignal + |> map { foundMessages -> (([Message], [PeerId: CombinedPeerReadState], Int32), Bool) in + updateSearchContext { _ in + return (ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil), true) + } + return ((foundMessages.messages, foundMessages.readStates, foundMessages.totalCount), false) + } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + |> then(loadMore) + ) + } + + let resolvedMessage = .single(nil) + |> then(context.sharedContext.resolveUrl(account: context.account, url: finalQuery) + |> mapToSignal { resolvedUrl -> Signal in + if case let .channelMessage(_, messageId) = resolvedUrl { + return downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: messageId) + } else { + return .single(nil) + } + }) + + return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage -> ([ChatListSearchEntry], Bool)? in + let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 + var entries: [ChatListSearchEntry] = [] + var index = 0 + + let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1)) + + let filteredPeer:(Peer, Peer) -> Bool = { peer, accountPeer in + guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } + guard !peersFilter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false } + guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } + + if peersFilter.contains(.onlyGroups) { + var isGroup: Bool = false + if let peer = peer as? TelegramChannel, case .group = peer.info { + isGroup = true + } else if peer.id.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + if !isGroup { + return false + } + } + + if peersFilter.contains(.onlyChannels) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + return true + } else { + return false + } + } + + if peersFilter.contains(.excludeChannels) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + return false + } + } + + return true + } + + var existingPeerIds = Set() + + var totalNumberOfLocalPeers = 0 + for renderedPeer in foundLocalPeers.peers { + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + totalNumberOfLocalPeers += 1 + } + } + } + for peer in foundRemotePeers.0 { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + existingPeerIds.insert(peer.peer.id) + totalNumberOfLocalPeers += 1 + } + } + + var totalNumberOfGlobalPeers = 0 + for peer in foundRemotePeers.1 { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + totalNumberOfGlobalPeers += 1 + } + } + + existingPeerIds.removeAll() + + let localExpandType: ChatListSearchSectionExpandType = .none + let globalExpandType: ChatListSearchSectionExpandType + if totalNumberOfGlobalPeers > 3 { + globalExpandType = searchState.expandGlobalSearch ? .collapse : .expand + } else { + globalExpandType = .none + } + + let lowercasedQuery = finalQuery.lowercased() + if lowercasedQuery.count > 1 && (presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery)) { + if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) { + existingPeerIds.insert(accountPeer.id) + entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + index += 1 + } + } + + var numberOfLocalPeers = 0 + for renderedPeer in foundLocalPeers.peers { + if case .expand = localExpandType, numberOfLocalPeers >= 3 { + break + } + + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + var associatedPeer: Peer? + if let associatedPeerId = peer.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 + } + } + } + + for peer in foundRemotePeers.0 { + if case .expand = localExpandType, numberOfLocalPeers >= 3 { + break + } + + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + existingPeerIds.insert(peer.peer.id) + entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + index += 1 + numberOfLocalPeers += 1 + } + } + + var numberOfGlobalPeers = 0 + index = 0 + if let _ = tagMask { + } else { + for peer in foundRemotePeers.1 { + if case .expand = globalExpandType, numberOfGlobalPeers >= 3 { + break + } + + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + existingPeerIds.insert(peer.peer.id) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) + index += 1 + numberOfGlobalPeers += 1 + } + } + } + + if let message = resolvedMessage { + var peer = RenderedPeer(message: message) + if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { + if let channelPeer = message.peers[migrationReference.peerId] { + peer = RenderedPeer(peer: channelPeer) + } + } + entries.append(.message(message, peer, nil, presentationData, 1, nil, true)) + index += 1 + } + + var firstHeaderId: Int64? + if !foundRemotePeers.2 { + index = 0 + for message in foundRemoteMessages.0.0 { + let headerId = listMessageDateHeaderId(timestamp: message.timestamp) + if firstHeaderId == nil { + firstHeaderId = headerId + } + var peer = RenderedPeer(message: message) + if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { + if let channelPeer = message.peers[migrationReference.peerId] { + peer = RenderedPeer(peer: channelPeer) + } + } + entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId)) + index += 1 + } + } + + if tagMask == nil, !peersFilter.contains(.excludeRecent), isViablePhoneNumber(finalQuery) { + entries.append(.addContact(finalQuery, presentationData.theme, presentationData.strings)) + } + + return (entries, isSearching) + } + } + + let foundMessages = searchContext.get() |> map { searchContext -> ([Message], Int32, Bool) in + if let result = searchContext?.result { + return (result.messages, result.totalCount, result.hasMore) + } else { + return ([], 0, false) + } + } + + let loadMore = { + updateSearchContext { previous in + guard let previous = previous else { + return (nil, false) + } + if previous.loadMoreIndex != nil { + return (previous, false) + } + guard let last = previous.result.messages.last else { + return (previous, false) + } + return (ChatListSearchMessagesContext(result: previous.result, loadMoreIndex: last.index), true) + } + } + + openMediaMessageImpl = { message, mode in + let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { + interaction.dismissInput() + }, present: { c, a in + interaction.present(c, a) + }, transitionNode: { messageId, media in + return transitionNodeImpl?(messageId, media) + }, addToTransitionSurface: { view in + addToTransitionSurfaceImpl?(view) + }, openUrl: { url in + interaction.openUrl(url) + }, openPeer: { peer, navigation in + //self?.openPeer(peerId: peer.id, navigation: navigation) + }, callPeer: { _, _ in + }, enqueueMessage: { _ in + }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, gallerySource: .custom(messages: foundMessages, messageId: message.id, loadMore: { + loadMore() + }))) + } + + transitionNodeImpl = { [weak self] messageId, media in + if let strongSelf = self { + return strongSelf.mediaNode.transitionNodeForGallery(messageId: messageId, media: media) + } else { + return nil + } + } + + addToTransitionSurfaceImpl = { [weak self] view in + if let strongSelf = self { + strongSelf.mediaNode.addToTransitionSurface(view: view) + } + } + + let chatListInteraction = ChatListNodeInteraction(activateSearch: { + }, peerSelected: { [weak self] peer, _ in + interaction.dismissInput() + interaction.openPeer(peer, false) + let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() + self?.listNode.clearHighlightAnimated(true) + }, disabledPeerSelected: { _ in + }, togglePeerSelected: { _ in + }, additionalCategorySelected: { _ in + }, messageSelected: { [weak self] peer, message, _ in + interaction.dismissInput() + if let peer = message.peers[message.id.peerId] { + interaction.openMessage(peer, message.id) + } + self?.listNode.clearHighlightAnimated(true) + }, groupSelected: { _ in + }, addContact: { [weak self] phoneNumber in + interaction.dismissInput() + interaction.addContact(phoneNumber) + self?.listNode.clearHighlightAnimated(true) + }, setPeerIdWithRevealedOptions: { _, _ in + }, setItemPinned: { _, _ in + }, setPeerMuted: { _, _ in + }, deletePeer: { _, _ in + }, updatePeerGrouping: { _, _ in + }, togglePeerMarkedUnread: { _, _ in + }, toggleArchivedFolderHiddenByDefault: { + }, hidePsa: { _ in + }, activateChatPreview: { item, node, gesture in + guard let peerContextAction = interaction.peerContextAction else { + gesture?.cancel() + return + } + switch item.content { + case let .peer(peer): + if let peer = peer.peer.peer { + peerContextAction(peer, .search, node, gesture) + } + case .groupReference: + gesture?.cancel() + } + }, present: { c in + interaction.present(c, nil) + }) + + let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in + interaction.dismissInput() + return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { + interaction.dismissInput() + }, present: { c, a in + interaction.present(c, a) + }, transitionNode: { [weak self] messageId, media in + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + if let strongSelf = self { + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + } + return transitionNode + }, addToTransitionSurface: { view in + self?.addToTransitionSurface(view: view) + }, openUrl: { url in + interaction.openUrl(url) + }, openPeer: { peer, navigation in +// interaction.openPeer(peer.id, navigation) + }, callPeer: { _, _ in + }, enqueueMessage: { _ in + }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .custom(messages: foundMessages, at: message.id, loadMore: { + loadMore() + }), gallerySource: .custom(messages: foundMessages, messageId: message.id, loadMore: { + loadMore() + }))) + }, openMessageContextMenu: { message, _, node, rect, gesture in + interaction.messageContextAction(message, node, rect, gesture) + }, toggleMessagesSelection: { messageId, selected in + if let messageId = messageId.first { + interaction.toggleMessageSelection(messageId, selected) + } + }, openUrl: { url, _, _, message in + interaction.openUrl(url) + }, openInstantPage: { [weak self] message, data in + if let (webpage, anchor) = instantPageAndAnchor(message: message) { + let pageController = InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor) + self?.navigationController?.pushViewController(pageController) + } + }, longTap: { action, message in + }, getHiddenMedia: { + return [:] + }) + + let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) + let previousSelectedMessages = Atomic?>(value: nil) + + let _ = (searchQuery + |> deliverOnMainQueue).start { [weak self] query in + self?.searchQueryValue = query + } + + let _ = (searchOptions + |> deliverOnMainQueue).start { [weak self] options in + self?.searchOptionsValue = options + } + + self.searchDisposable.set((foundItems + |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags in + if let strongSelf = self { + let previousSelectedMessageIds = previousSelectedMessages.swap(strongSelf.selectedMessages) + + let isSearching = entriesAndFlags?.1 ?? false + strongSelf._isSearching.set(isSearching) + + if strongSelf.tagMask == .photoOrVideo { + var totalCount: Int32 = 0 + if let entries = entriesAndFlags?.0 { + for entry in entries { + if case let .message(_, _, _, _, count, _, _) = entry { + totalCount = count + break + } + } + } + var entries: [ChatListSearchEntry]? = entriesAndFlags?.0 ?? [] + if isSearching && (entries?.isEmpty ?? true) { + entries = nil + } + strongSelf.mediaNode.updateHistory(entries: entries, totalCount: totalCount, updateType: .Initial) + } + + var entriesAndFlags = entriesAndFlags + + var peers: [Peer] = [] + if let entries = entriesAndFlags?.0 { + var filteredEntries: [ChatListSearchEntry] = [] + for entry in entries { + if case let .localPeer(peer, _, _, _, _, _, _, _, _) = entry { + peers.append(peer) + } else { + filteredEntries.append(entry) + } + } + + if strongSelf.tagMask != nil || strongSelf.searchOptionsValue?.maxDate != nil || strongSelf.searchOptionsValue?.peer != nil { + entriesAndFlags?.0 = filteredEntries + } + } + + let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) + let newEntries = entriesAndFlags?.0 ?? [] + + let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) + let firstTime = previousEntries == nil + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, searchQuery: strongSelf.searchQueryValue ?? "", context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { _, _, _, _ in }, toggleExpandLocalResults: { + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.expandLocalSearch = !state.expandLocalSearch + return state + } + }, toggleExpandGlobalResults: { + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.expandGlobalSearch = !state.expandGlobalSearch + return state + } + }, searchPeer: { peer in + }, searchResults: newEntries.compactMap { entry -> Message? in + if case let .message(message, _, _, _, _, _, _) = entry { + return message + } else { + return nil + } + }, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in + interaction.messageContextAction(message, node, rect, gesture) + }) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + + var messages: [Message] = [] + for entry in newEntries { + if case let .message(message, _, _, _, _, _, _) = entry { + messages.append(message) + } + } + strongSelf.searchCurrentMessages = messages + + if !strongSelf.didSetReady { + strongSelf.ready.set(.single(true)) + strongSelf.didSetReady = true + } else if tagMask != nil { + interaction.updateSuggestedPeers(Array(peers.prefix(8))) + } + } + })) + + let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) + let hasRecentPeers = recentPeers(account: context.account) + |> map { value -> Bool in + switch value { + case let .peers(peers): + return !peers.isEmpty + case .disabled: + return false + } + } + |> distinctUntilChanged + + let previousRecentlySearchedPeerOrder = Atomic<[PeerId]>(value: []) + let fixedRecentlySearchedPeers = recentlySearchedPeers(postbox: context.account.postbox) + |> map { peers -> [RecentlySearchedPeer] in + var result: [RecentlySearchedPeer] = [] + let _ = previousRecentlySearchedPeerOrder.modify { current in + var updated: [PeerId] = [] + for id in current { + inner: for peer in peers { + if peer.peer.peerId == id { + updated.append(id) + result.append(peer) + break inner + } + } + } + for peer in peers.reversed() { + if !updated.contains(peer.peer.peerId) { + updated.insert(peer.peer.peerId, at: 0) + result.insert(peer, at: 0) + } + } + return updated + } + return result + } + + var recentItems = combineLatest(hasRecentPeers, fixedRecentlySearchedPeers, presentationDataPromise.get()) + |> mapToSignal { hasRecentPeers, peers, presentationData -> Signal<[ChatListRecentEntry], NoError> in + var entries: [ChatListRecentEntry] = [] + if !peersFilter.contains(.onlyGroups) { + if hasRecentPeers { + entries.append(.topPeers([], presentationData.theme, presentationData.strings)) + } + } + var peerIds = Set() + var index = 0 + loop: for searchedPeer in peers { + if let peer = searchedPeer.peer.peers[searchedPeer.peer.peerId] { + if peerIds.contains(peer.id) { + continue loop + } + if !doesPeerMatchFilter(peer: peer, filter: peersFilter) { + continue + } + peerIds.insert(peer.id) + + entries.append(.peer(index: index, peer: searchedPeer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + index += 1 + } + } + + return .single(entries) + } + + if peersFilter.contains(.excludeRecent) { + recentItems = .single([]) + } + + if tagMask == nil && !peersFilter.contains(.excludeRecent) { + self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) + } + + self.recentDisposable.set((combineLatest(queue: .mainQueue(), + presentationDataPromise.get(), + recentItems + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, entries in + if let strongSelf = self { + let previousEntries = previousRecentItems.swap(entries) + + let firstTime = previousEntries == nil + let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: peersFilter, peerSelected: { peer in + interaction.openPeer(peer, true) + let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() + self?.recentListNode.clearHighlightAnimated(true) + }, disabledPeerSelected: { peer in + interaction.openDisabledPeer(peer) + }, peerContextAction: { peer, source, node, gesture in + if let peerContextAction = interaction.peerContextAction { + peerContextAction(peer, source, node, gesture) + } else { + gesture?.cancel() + } + }, clearRecentlySearchedPeers: { + interaction.clearRecentSearch() + }, deletePeer: { peerId in + if let strongSelf = self { + let _ = removeRecentlySearchedPeer(postbox: strongSelf.context.account.postbox, peerId: peerId).start() + } + }) + strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) + } + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + strongSelf.presentationData = presentationData + strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations))) + + if previousTheme !== presentationData.theme { +// strongSelf.updateTheme(theme: presentationData.theme) + } + } + }) + + self.recentListNode.beganInteractiveDragging = { + interaction.dismissInput() + } + + self.listNode.beganInteractiveDragging = { + interaction.dismissInput() + } + + self.mediaNode.beganInteractiveDragging = { + interaction.dismissInput() + } + + self.listNode.visibleBottomContentOffsetChanged = { offset in + guard case let .known(value) = offset, value < 160.0 else { + return + } + loadMore() + } + + self.mediaNode.loadMore = { + loadMore() + } + + if tagMask == .music || tagMask == .voiceOrInstantVideo { + self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState + |> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in + if let (account, state, type) = playlistStateAndType { + switch state { + case let .state(state): + if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .custom = playlistId { + switch type { + case .voice: + if tagMask != .voiceOrInstantVideo { + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + case .music: + if tagMask != .music { + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + } + return .single((account, state, type)) + } else { + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + case .loading: + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + } else { + return .single(nil) + } + } + |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in + guard let strongSelf = self else { + return + } + if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) || + !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) || + !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) || + strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 { + + if let playlistStateAndType = playlistStateAndType { + strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0) + } else { + strongSelf.playlistStateAndType = nil + } + + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }) + } + } + + deinit { + self.presentationDataDisposable?.dispose() + self.searchDisposable.dispose() + self.hiddenMediaDisposable?.dispose() + self.mediaStatusDisposable?.dispose() + self.playlistPreloadDisposable?.dispose() + self.recentDisposable.dispose() + self.updatedRecentPeersDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.emptyResultsAnimationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:)))) + } + + private func updateState(_ f: (ChatListSearchListPaneNodeState) -> ChatListSearchListPaneNodeState) { + let state = f(self.searchStateValue) + if state != self.searchStateValue { + self.searchStateValue = state + self.searchStatePromise.set(state) + } + } + + @objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, !self.emptyResultsAnimationNode.isPlaying { + let _ = self.emptyResultsAnimationNode.playIfNeeded() + } + } + + func scrollToTop() -> Bool { + let offset = self.listNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: +// self.listNode.scrollto + return true + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + var hadValidLayout = self.currentParams != nil + self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData) + + var topPanelHeight: CGFloat = 0.0 + if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType { + let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight + topPanelHeight = panelHeight + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - panelHeight), size: CGSize(width: size.width, height: panelHeight)) + if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { + transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) + mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) + switch order { + case .regular: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) + case .reversed: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) + case .random: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) + } + let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState + |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in + guard let value = value else { + return .single(nil) + } + switch value.1 { + case .state: + return .single(value) + case .loading: + return .single(value) |> delay(0.1, queue: .mainQueue()) + } + } + + mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus + |> map { state -> MediaPlayerStatus in + if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { + return state.status + } else { + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + } + } + } else { + if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { + self.mediaAccessoryPanel = nil + self.dismissingPanel = mediaAccessoryPanel + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in + mediaAccessoryPanel?.removeFromSupernode() + if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { + strongSelf.dismissingPanel = nil + } + }) + } + + let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context) + mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo + mediaAccessoryPanel.close = { [weak self] in + if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { + strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) + } + } + mediaAccessoryPanel.toggleRate = { + [weak self] in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings) as? MusicPlaybackSettings ?? MusicPlaybackSettings.defaultSettings + + let nextRate: AudioPlaybackRate + switch settings.voicePlaybackRate { + case .x1: + nextRate = .x2 + case .x2: + nextRate = .x1 + } + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in + return settings.withUpdatedVoicePlaybackRate(nextRate) + }) + return nextRate + } + |> deliverOnMainQueue).start(next: { baseRate in + guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else { + return + } + strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type) + }) + } + mediaAccessoryPanel.togglePlayPause = { [weak self] in + if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { + strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) + } + } + mediaAccessoryPanel.playPrevious = { [weak self] in + if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { + strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type) + } + } + mediaAccessoryPanel.playNext = { [weak self] in + if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { + strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type) + } + } + mediaAccessoryPanel.tapAction = { [weak self] in + guard let strongSelf = self, let navigationController = strongSelf.navigationController, let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else { + return + } + if let id = state.id as? PeerMessagesMediaPlaylistItemId { + if type == .music { + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60), id: 0), context: strongSelf.context, chatLocation: .peer(id.messageId.peerId), chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) + + var cancelImpl: (() -> Void)? + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.interaction.present(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = MetaDisposable() + var progressStarted = false + strongSelf.playlistPreloadDisposable?.dispose() + strongSelf.playlistPreloadDisposable = (signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).start(next: { index in + guard let strongSelf = self else { + return + } + if let _ = index.0 { + let controllerContext: AccountContext + if account.id == strongSelf.context.account.id { + controllerContext = strongSelf.context + } else { + controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) + } + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, isGlobalSearch: false, parentNavigationController: navigationController) + strongSelf.view.window?.endEditing(true) + strongSelf.interaction.dismissInput() + strongSelf.interaction.present(controller, nil) + } else if index.1 { + if !progressStarted { + progressStarted = true + progressDisposable.set(progressSignal.start()) + } + } + }, completed: { + }) + cancelImpl = { + self?.playlistPreloadDisposable?.dispose() + } + } else { + strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) + } + } + } + mediaAccessoryPanel.frame = panelFrame + if let dismissingPanel = self.dismissingPanel { + self.mediaAccessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) + } else { + self.mediaAccessoryPanelContainer.addSubnode(mediaAccessoryPanel) + } + self.mediaAccessoryPanel = (mediaAccessoryPanel, type) + mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: .immediate) + switch order { + case .regular: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) + case .reversed: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) + case .random: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) + } + mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState + |> map { state -> MediaPlayerStatus in + if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { + return state.status + } else { + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + } + } + mediaAccessoryPanel.animateIn(transition: transition) + } + } else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { + self.mediaAccessoryPanel = nil + self.dismissingPanel = mediaAccessoryPanel + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in + mediaAccessoryPanel?.removeFromSupernode() + if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { + strongSelf.dismissingPanel = nil + } + }) + } + + transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight))) + + var topInset: CGFloat = 0.0 + let navigationBarHeight: CGFloat = 0.0 + let insets = UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset) + + self.loadingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: 422.0)) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.recentListNode.frame = CGRect(origin: CGPoint(), size: size) + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + self.listNode.frame = CGRect(origin: CGPoint(), size: size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + self.mediaNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height)) + self.mediaNode.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition) + + let padding: CGFloat = 16.0 + let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + var emptyAnimationHeight = self.animationSize.height + var emptyAnimationSpacing: CGFloat = 8.0 +// if case .landscape = layout.orientation, case .compact = layout.metrics.widthClass { +// emptyAnimationHeight = 0.0 +// emptyAnimationSpacing = 0.0 +// } + let emptyTextSpacing: CGFloat = 8.0 + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyAnimationY = navigationBarHeight + floorToScreenPixels((visibleHeight - navigationBarHeight - bottomInset - emptyTotalHeight) / 2.0) + + let textTransition = ContainedViewLayoutTransition.immediate + textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) + textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + self.emptyResultsAnimationNode.updateLayout(size: self.animationSize) + + if !hadValidLayout { + while !self.enqueuedRecentTransitions.isEmpty { + self.dequeueRecentTransition() + } + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + func updateHiddenMedia() { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } + } + } + + func cancelPreviewGestures() { + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + return transitionNode + } + + func addToTransitionSurface(view: UIView) { + self.view.addSubview(view) + } + + func updateSelectedMessages(animated: Bool) { + self.selectedMessages = self.interaction.getSelectedMessageIds() + self.mediaNode.selectedMessageIds = self.selectedMessages + self.mediaNode.updateSelectedMessages(animated: animated) + } + + private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) { + self.enqueuedRecentTransitions.append((transition, firstTime)) + + if self.currentParams != nil { + while !self.enqueuedRecentTransitions.isEmpty { + self.dequeueRecentTransition() + } + } + } + + private func dequeueRecentTransition() { + if let (transition, firstTime) = self.enqueuedRecentTransitions.first { + self.enqueuedRecentTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.PreferSynchronousDrawing) + } else { + options.insert(.AnimateInsertion) + } + + self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + } + + private func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) { + self.enqueuedTransitions.append((transition, firstTime)) + + if self.currentParams != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, _) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + options.insert(.PreferSynchronousResourceLoading) + + if transition.animated { + options.insert(.AnimateInsertion) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + let searchOptions = strongSelf.searchOptionsValue + strongSelf.listNode.isHidden = strongSelf.tagMask == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty + strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden + + let displayingResults = transition.displayingResults + if !displayingResults { + strongSelf.listNode.isHidden = true + strongSelf.mediaNode.isHidden = true + } + + let emptyResults = displayingResults && transition.isEmpty + if emptyResults { + let emptyResultsTitle: String + let emptyResultsText: String + if !transition.query.isEmpty { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0 + } else { + if let searchOptions = searchOptions, searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peer == nil { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter + if strongSelf.tagMask == .photoOrVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia + } else if strongSelf.tagMask == .webPage { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks + } else if strongSelf.tagMask == .file { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles + } else if strongSelf.tagMask == .music { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic + } else if strongSelf.tagMask == .voiceOrInstantVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + } else { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } + } else { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } + } + + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + } + + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) + } + + strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + strongSelf.emptyResultsAnimationNode.visibility = emptyResults + + if strongSelf.tagMask == .webPage { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Links") + } else if strongSelf.tagMask == .file { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Files") + } else if strongSelf.tagMask == .music || strongSelf.tagMask == .voiceOrInstantVideo { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Music") + } + + strongSelf.loadingNode.isHidden = !transition.isLoading + + strongSelf.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent) +// strongSelf.dimNode.isHidden = displayingResults + strongSelf.backgroundColor = !displayingResults && strongSelf.peersFilter.contains(.excludeRecent) ? nil : strongSelf.presentationData.theme.chatList.backgroundColor + } + }) + } + } + + func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? { + var selectedItemNode: ASDisplayNode? + var bounds: CGRect + if !self.recentListNode.isHidden { + let adjustedLocation = self.convert(location, to: self.recentListNode) + self.recentListNode.forEachItemNode { itemNode in + if itemNode.frame.contains(adjustedLocation) { + selectedItemNode = itemNode + } + } + } else { + let adjustedLocation = self.convert(location, to: self.listNode) + self.listNode.forEachItemNode { itemNode in + if itemNode.frame.contains(adjustedLocation) { + selectedItemNode = itemNode + } + } + } + if let selectedItemNode = selectedItemNode as? ChatListRecentPeersListItemNode { + if let result = selectedItemNode.viewAndPeerAtPoint(self.convert(location, to: selectedItemNode)) { + return (result.0, result.0.bounds, result.1) + } + } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.chatPeer { + if selectedItemNode.frame.height > 50.0 { + bounds = CGRect(x: 0.0, y: selectedItemNode.frame.height - 50.0, width: selectedItemNode.frame.width, height: 50.0) + } else { + bounds = selectedItemNode.bounds + } + return (selectedItemNode.view, bounds, peer.id) + } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { + if selectedItemNode.frame.height > 76.0 { + bounds = CGRect(x: 0.0, y: selectedItemNode.frame.height - 76.0, width: selectedItemNode.frame.width, height: 76.0) + } else { + bounds = selectedItemNode.bounds + } + switch item.content { + case let .peer(messages, peer, _, _, _, _, _, _, _, _, _, _): + return (selectedItemNode.view, bounds, messages.last?.id ?? peer.peerId) + case let .groupReference(groupId, _, _, _, _): + return (selectedItemNode.view, bounds, groupId) + } + } + return nil + } +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift index 93d4b11ac2..595f0c060d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift @@ -117,6 +117,7 @@ final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode { } func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout if presentationData.theme !== self.theme { self.theme = presentationData.theme diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift new file mode 100644 index 0000000000..8baac422d4 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -0,0 +1,539 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import SyncCore +import TelegramCore +import AccountContext +import ContextUI + +protocol ChatListSearchPaneNode: ASDisplayNode { + var isReady: Signal { get } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func scrollToTop() -> Bool + func cancelPreviewGestures() + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + func addToTransitionSurface(view: UIView) + func updateHiddenMedia() + func updateSelectedMessages(animated: Bool) + func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? + var searchCurrentMessages: [Message]? { get } +} + +final class ChatListSearchPaneWrapper { + let key: ChatListSearchPaneKey + let node: ChatListSearchPaneNode + var isAnimatingOut: Bool = false + private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, PresentationData)? + + init(key: ChatListSearchPaneKey, node: ChatListSearchPaneNode) { + self.key = key + self.node = node + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentPresentationData) = self.appliedParams { + if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset && currentPresentationData === presentationData { + return + } + } + self.appliedParams = (size, sideInset, bottomInset, visibleHeight, presentationData) + self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: synchronous, transition: transition) + } +} + +enum ChatListSearchPaneKey { + case chats + case media + case links + case files + case music + case voice +} + +private let availablePanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice] + +struct ChatListSearchPaneSpecifier: Equatable { + var key: ChatListSearchPaneKey + var title: String +} + +private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { + return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) +} + +private final class ChatListSearchPendingPane { + let pane: ChatListSearchPaneWrapper + private var disposable: Disposable? + var isReady: Bool = false + + init( + context: AccountContext, + interaction: ChatListSearchInteraction, + navigationController: NavigationController?, + peersFilter: ChatListNodePeersFilter, + searchQuery: Signal, + searchOptions: Signal, + key: ChatListSearchPaneKey, + hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void + ) { + let paneNode: ChatListSearchPaneNode + switch key { + case .chats: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: nil, peersFilter: peersFilter, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + case .media: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: .photoOrVideo, peersFilter: [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + case .files: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: .file, peersFilter: [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + case .links: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: .webPage, peersFilter: [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + case .voice: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: .voiceOrInstantVideo, peersFilter: [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + case .music: + paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, tagMask: .music, peersFilter: [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + } + + self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) + self.disposable = (paneNode.isReady + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.isReady = true + hasBecomeReady(key) + }) + } + + deinit { + self.disposable?.dispose() + } +} + +final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { + private let context: AccountContext + private let peersFilter: ChatListNodePeersFilter + private let searchQuery: Signal + private let searchOptions: Signal + private let navigationController: NavigationController? + var interaction: ChatListSearchInteraction? + + private let coveringBackgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + + let isReady = Promise() + var didSetIsReady = false + + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)? + + private(set) var currentPaneKey: ChatListSearchPaneKey? + var pendingSwitchToPaneKey: ChatListSearchPaneKey? + + var currentPane: ChatListSearchPaneWrapper? { + if let currentPaneKey = self.currentPaneKey { + return self.currentPanes[currentPaneKey] + } else { + return nil + } + } + + private var currentPanes: [ChatListSearchPaneKey: ChatListSearchPaneWrapper] = [:] + private var pendingPanes: [ChatListSearchPaneKey: ChatListSearchPendingPane] = [:] + + private var transitionFraction: CGFloat = 0.0 + + var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? + + var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)? + var requestExpandTabs: (() -> Bool)? + + private var currentAvailablePanes: [ChatListSearchPaneKey]? + + init(context: AccountContext, peersFilter: ChatListNodePeersFilter, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + self.context = context + self.peersFilter = peersFilter + self.searchQuery = searchQuery + self.searchOptions = searchOptions + self.navigationController = navigationController + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.coveringBackgroundNode = ASDisplayNode() + self.coveringBackgroundNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.coveringBackgroundNode) + } + + func requestSelectPane(_ key: ChatListSearchPaneKey) { + if self.currentPaneKey == key { + if let requestExpandTabs = self.requestExpandTabs, requestExpandTabs() { + } else { + let _ = self.currentPane?.node.scrollToTop() + } + return + } + if self.currentPanes[key] != nil { + self.currentPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = self.currentParams { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, transition: .animated(duration: 0.4, curve: .spring)) + } + } else if self.pendingSwitchToPaneKey != key { + self.pendingSwitchToPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = self.currentParams { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in + guard let strongSelf = self, let currentPaneKey = strongSelf.currentPaneKey, let index = availablePanes.firstIndex(of: currentPaneKey) else { + return [] + } + if index == 0 { + return .left + } + return [.left, .right] + }) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(panRecognizer) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } + } + + cancelContextGestures(view: self.view) + case .changed: + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + var transitionFraction = translation.x / size.width + if currentIndex <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if currentIndex >= availablePanes.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, transition: .immediate) + } + case .cancelled, .ended: + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + var directionIsToRight: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + if abs(translation.x) > size.width / 2.0 { + directionIsToRight = translation.x > size.width / 2.0 + } + } + var updated = false + if let directionIsToRight = directionIsToRight { + var updatedIndex = currentIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, availablePanes.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + let switchToKey = availablePanes[updatedIndex] + if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ + self.currentPaneKey = switchToKey + updated = true + } + } + self.transitionFraction = 0.0 + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, transition: .animated(duration: 0.35, curve: .spring)) + } + default: + break + } + } + + func scrollToTop() -> Bool { + if let currentPane = self.currentPane { + return currentPane.node.scrollToTop() + } else { + return false + } + } + + func updateHiddenMedia() { + self.currentPane?.node.updateHiddenMedia() + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) + } + + func updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { + for (_, pane) in self.currentPanes { + pane.node.updateSelectedMessages(animated: animated) + } + for (_, pane) in self.pendingPanes { + pane.pane.node.updateSelectedMessages(animated: animated) + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + let previousAvailablePanes = self.currentAvailablePanes ?? [] + self.currentAvailablePanes = availablePanes + + if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { + var nextCandidatePaneKey: ChatListSearchPaneKey? + if let index = previousAvailablePanes.firstIndex(of: currentPaneKey), index != 0 { + for i in (0 ... index - 1).reversed() { + if availablePanes.contains(previousAvailablePanes[i]) { + nextCandidatePaneKey = previousAvailablePanes[i] + } + } + } + if nextCandidatePaneKey == nil { + nextCandidatePaneKey = availablePanes.first + } + + if let nextCandidatePaneKey = nextCandidatePaneKey { + self.pendingSwitchToPaneKey = nextCandidatePaneKey + } else { + self.currentPaneKey = nil + self.pendingSwitchToPaneKey = nil + } + } else if self.currentPaneKey == nil { + self.pendingSwitchToPaneKey = availablePanes.first + } + + let currentIndex: Int? + if let currentPaneKey = self.currentPaneKey { + currentIndex = availablePanes.firstIndex(of: currentPaneKey) + } else { + currentIndex = nil + } + + self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData) + + transition.updateAlpha(node: self.coveringBackgroundNode, alpha: 0.0) + + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let tabsHeight: CGFloat = 48.0 + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) + + let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + + var visiblePaneIndices: [Int] = [] + var requiredPendingKeys: [ChatListSearchPaneKey] = [] + if let currentIndex = currentIndex { + if currentIndex != 0 { + visiblePaneIndices.append(currentIndex - 1) + } + visiblePaneIndices.append(currentIndex) + if currentIndex != availablePanes.count - 1 { + visiblePaneIndices.append(currentIndex + 1) + } + + for index in visiblePaneIndices { + let key = availablePanes[index] + if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { + requiredPendingKeys.append(key) + } + } + } + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey { + if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { + if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { + requiredPendingKeys.append(pendingSwitchToPaneKey) + } + } + } + + for key in requiredPendingKeys { + if self.pendingPanes[key] == nil { + var leftScope = false + let pane = ChatListSearchPendingPane( + context: self.context, + interaction: self.interaction!, + navigationController: self.navigationController, + peersFilter: self.peersFilter, + searchQuery: self.searchQuery, + searchOptions: self.searchOptions, + key: key, + hasBecomeReady: { [weak self] key in + let apply: () -> Void = { + guard let strongSelf = self else { + return + } + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { + var transition: ContainedViewLayoutTransition = .immediate + if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { + transition = .animated(duration: 0.4, curve: .spring) + } + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, transition: transition) + } + } + if leftScope { + apply() + } + } + ) + self.pendingPanes[key] = pane + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .immediate) + leftScope = true + } + } + + for (key, pane) in self.pendingPanes { + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) + + if pane.isReady { + self.pendingPanes.removeValue(forKey: key) + self.currentPanes[key] = pane.pane + } + } + + var paneDefaultTransition = transition + var previousPaneKey: ChatListSearchPaneKey? + var paneSwitchAnimationOffset: CGFloat = 0.0 + + var updatedCurrentIndex = currentIndex + var animatePaneTransitionOffset: CGFloat? + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let pane = self.currentPanes[pendingSwitchToPaneKey] { + self.pendingSwitchToPaneKey = nil + previousPaneKey = self.currentPaneKey + self.currentPaneKey = pendingSwitchToPaneKey + updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) + if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { + if updatedCurrentIndex < previousIndex { + paneSwitchAnimationOffset = -size.width + } else { + paneSwitchAnimationOffset = size.width + } + } + + paneDefaultTransition = .immediate + } + + for (key, pane) in self.currentPanes { + if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { + var paneWasAdded = false + if pane.node.supernode == nil { + self.addSubnode(pane.node) + paneWasAdded = true + } + let indexOffset = CGFloat(index - updatedCurrentIndex) + + let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition + let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0) + + let paneCompletion: () -> Void = { [weak self, weak pane] in + guard let strongSelf = self, let pane = pane else { + return + } + pane.isAnimatingOut = false + if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { + if let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { + } else { + if let pane = strongSelf.currentPanes.removeValue(forKey: key) { + pane.node.removeFromSupernode() + } + } + } + } + if let previousPaneKey = previousPaneKey, key == previousPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } else if let previousPaneKey = previousPaneKey, key == self.currentPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : { + paneCompletion() + }) + } else { + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } + pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + } + } + + for (_, pane) in self.pendingPanes { + let paneTransition: ContainedViewLayoutTransition = .immediate + paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: paneTransition) + } + if !self.didSetIsReady { + if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { + self.didSetIsReady = true + self.isReady.set(currentPane.node.isReady) + } else if self.pendingSwitchToPaneKey == nil { + self.didSetIsReady = true + self.isReady.set(.single(true)) + } + } + + self.currentPaneUpdated?(self.currentPaneKey, self.transitionFraction, transition) + } + + func allCurrentMessages() -> [MessageId: Message] { + var allMessages: [MessageId: Message] = [:] + for (_, pane) in self.currentPanes { + if let messages = pane.node.searchCurrentMessages { + for message in messages { + allMessages[message.id] = message + } + } + } + return allMessages + } +} diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index e72d01226a..164ffa37cf 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -20,7 +20,7 @@ import LocalizedPeerData import TextSelectionNode private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) -private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: .white) +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white) private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white) @@ -439,7 +439,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } - func setMessage(_ message: Message) { + func setMessage(_ message: Message, displayInfo: Bool = true) { self.currentMessage = message let canDelete: Bool @@ -464,14 +464,18 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } var authorNameText: String? - if let author = message.effectiveAuthor { authorNameText = author.displayTitle(strings: self.strings, displayOrder: self.nameOrder) } else if let peer = message.peers[message.id.peerId] { authorNameText = peer.displayTitle(strings: self.strings, displayOrder: self.nameOrder) } - let dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp) + var dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp) + + if !displayInfo { + authorNameText = "" + dateText = "" + } var messageText = NSAttributedString(string: "") var hasCaption = false @@ -495,7 +499,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.actionButton.isHidden = message.containsSecretMedia || Namespaces.Message.allScheduled.contains(message.id.namespace) - if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { self.currentMessageText = messageText diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index c785e51ba3..16037d1210 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -143,12 +143,12 @@ private func galleryMessageCaptionText(_ message: Message) -> String { return message.text } -public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void = { _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? { +public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void = { _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? { let message = entry.message let location = entry.location if let (media, mediaImage) = mediaForMessage(message: message) { if let _ = media as? TelegramMediaImage { - return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions, present: present) + return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, present: present) } else if let file = media as? TelegramMediaFile { if file.isVideo { let content: UniversalVideoContent @@ -176,7 +176,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities) - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) } else { if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) @@ -187,7 +187,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese pixelsCount = Int(dimensions.width) * Int(dimensions.height) } if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 { - return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions, present: present) + return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, present: present) } else { return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location) } @@ -215,7 +215,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } } if let content = content { - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) } else { return nil } @@ -453,7 +453,7 @@ public class GalleryController: ViewController, StandalonePresentableController var entries: [MessageHistoryEntry] = [] var index = messages.count for message in messages.reversed() { - entries.append(MessageHistoryEntry(message: message, isRead: false, location: MessageHistoryEntryLocation(index: Int(totalCount) - index, count: Int(totalCount)), monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) + entries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) index -= 1 } return GalleryMessageHistoryView.entries(entries, hasMore, false) @@ -469,6 +469,11 @@ public class GalleryController: ViewController, StandalonePresentableController semaphore = nil } + var displayInfoOnTop = false + if case .custom = source { + displayInfoOnTop = true + } + let syncResult = Atomic<(Bool, (() -> Void)?)>(value: (false, nil)) self.disposable.set(combineLatest(messageView, self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])).start(next: { [weak self] view, preferencesView in let f: () -> Void = { @@ -524,7 +529,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -961,6 +966,11 @@ public class GalleryController: ViewController, StandalonePresentableController return baseNavigationController } + var displayInfoOnTop = false + if case .custom = source { + displayInfoOnTop = true + } + var items: [GalleryItem] = [] var centralItemIndex: Int? for entry in self.entries { @@ -968,7 +978,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == self.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -1048,7 +1058,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -1078,7 +1088,7 @@ public class GalleryController: ViewController, StandalonePresentableController var entries: [MessageHistoryEntry] = [] var index = messages.count for message in messages.reversed() { - entries.append(MessageHistoryEntry(message: message, isRead: false, location: MessageHistoryEntryLocation(index: Int(totalCount) - index, count: Int(totalCount)), monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) + entries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) index -= 1 } @@ -1100,7 +1110,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift new file mode 100644 index 0000000000..53848f00a4 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -0,0 +1,62 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramPresentationData +import TelegramStringFormatting + +private let titleFont = Font.medium(15.0) +private let dateFont = Font.regular(14.0) + +final class GalleryTitleView: UIView, NavigationBarTitleView { + private let authorNameNode: ASTextNode + private let dateNode: ASTextNode + + override init(frame: CGRect) { + self.authorNameNode = ASTextNode() + self.authorNameNode.displaysAsynchronously = false + self.authorNameNode.maximumNumberOfLines = 1 + + self.dateNode = ASTextNode() + self.dateNode.displaysAsynchronously = false + self.dateNode.maximumNumberOfLines = 1 + + super.init(frame: frame) + + self.addSubnode(self.authorNameNode) + self.addSubnode(self.dateNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setMessage(_ message: Message, presentationData: PresentationData, accountPeerId: PeerId) { + let authorNameText = stringForFullAuthorName(message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId) + let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: message.timestamp) + + self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) + self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) + } + + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { + let leftInset: CGFloat = 0.0 + let rightInset: CGFloat = 0.0 + + let authorNameSize = self.authorNameNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) + let dateSize = self.dateNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + if authorNameSize.height.isZero { + self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height) / 2.0)), size: dateSize) + } else { + let labelsSpacing: CGFloat = 0.0 + self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((size.width - authorNameSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize) + self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) + } + } + + func animateLayoutTransition() { + + } +} diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index d582cc1883..d04b34b160 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -87,15 +87,17 @@ class ChatImageGalleryItem: GalleryItem { let presentationData: PresentationData let message: Message let location: MessageHistoryEntryLocation? + let displayInfoOnTop: Bool let performAction: (GalleryControllerInteractionTapAction) -> Void let openActionOptions: (GalleryControllerInteractionTapAction) -> Void let present: (ViewController, Any?) -> Void - init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.message = message self.location = location + self.displayInfoOnTop = displayInfoOnTop self.performAction = performAction self.openActionOptions = openActionOptions self.present = present @@ -125,8 +127,11 @@ class ChatImageGalleryItem: GalleryItem { if let location = self.location { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").0)) } - - node.setMessage(self.message) + + if self.displayInfoOnTop { + node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + } + node.setMessage(self.message, displayInfo: !self.displayInfoOnTop) return node } @@ -134,8 +139,11 @@ class ChatImageGalleryItem: GalleryItem { func updateNode(node: GalleryItemNode, synchronous: Bool) { if let node = node as? ChatImageGalleryItemNode, let location = self.location { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").0)) - - node.setMessage(self.message) + + if self.displayInfoOnTop { + node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + } + node.setMessage(self.message, displayInfo: !self.displayInfoOnTop) } } @@ -167,10 +175,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var tilingNode: TilingNode? fileprivate let _ready = Promise() fileprivate let _title = Promise() + fileprivate let _titleView = Promise() fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>(nil) private let statusNodeContainer: HighlightableButtonNode private let statusNode: RadialStatusNode private let footerContentNode: ChatItemGalleryFooterContentNode + fileprivate var titleContentView: GalleryTitleView? private var contextAndMedia: (AccountContext, AnyMediaReference)? @@ -207,6 +217,9 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) self.statusNodeContainer.isUserInteractionEnabled = false + + self.titleContentView = GalleryTitleView(frame: CGRect()) + self._titleView.set(.single(self.titleContentView)) } deinit { @@ -227,8 +240,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } - fileprivate func setMessage(_ message: Message) { - self.footerContentNode.setMessage(message) + fileprivate func setMessage(_ message: Message, displayInfo: Bool) { + self.footerContentNode.setMessage(message, displayInfo: displayInfo) } fileprivate func setImage(imageReference: ImageMediaReference) { @@ -579,6 +592,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { return self._title.get() } + override func titleView() -> Signal { + return self._titleView.get() + } + override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> { return self._rightBarButtonItems.get() } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index f5e21124f7..51c829cb97 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -34,6 +34,7 @@ public class UniversalVideoGalleryItem: GalleryItem { let contentInfo: UniversalVideoGalleryItemContentInfo? let caption: NSAttributedString let credit: NSAttributedString? + let displayInfoOnTop: Bool let hideControls: Bool let fromPlayingVideo: Bool let landscape: Bool @@ -45,7 +46,7 @@ public class UniversalVideoGalleryItem: GalleryItem { let storeMediaPlaybackState: (MessageId, Double?) -> Void let present: (ViewController, Any?) -> Void - public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, present: @escaping (ViewController, Any?) -> Void) { + public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.content = content @@ -54,6 +55,7 @@ public class UniversalVideoGalleryItem: GalleryItem { self.contentInfo = contentInfo self.caption = caption self.credit = credit + self.displayInfoOnTop = displayInfoOnTop self.hideControls = hideControls self.fromPlayingVideo = fromPlayingVideo self.landscape = landscape @@ -75,6 +77,10 @@ public class UniversalVideoGalleryItem: GalleryItem { node.setupItem(self) + if self.displayInfoOnTop, case let .message(message) = self.contentInfo { + node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + } + return node } @@ -85,6 +91,10 @@ public class UniversalVideoGalleryItem: GalleryItem { } node.setupItem(self) + + if self.displayInfoOnTop, case let .message(message) = self.contentInfo { + node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + } } } @@ -249,6 +259,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _titleView = Promise() fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>() + fileprivate var titleContentView: GalleryTitleView? private let scrubberView: ChatVideoGalleryItemScrubberView private let footerContentNode: ChatItemGalleryFooterContentNode private let overlayContentNode: UniversalVideoGalleryItemOverlayNode @@ -309,7 +320,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) self._title.set(.single("")) - self._titleView.set(.single(nil)) super.init() @@ -412,6 +422,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.pictureInPictureButtonPressed() return true } + + self.titleContentView = GalleryTitleView(frame: CGRect()) + self._titleView.set(.single(self.titleContentView)) } deinit { @@ -756,7 +769,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let contentInfo = item.contentInfo { switch contentInfo { case let .message(message): - self.footerContentNode.setMessage(message) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 45c43d96d7..661e505d73 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -88,18 +88,15 @@ public final class HashtagSearchController: TelegramBaseController { }, openMessageContextMenu: { message, bool, node, rect, gesture in }, toggleMessagesSelection: { messageId, selected in - }, openUrl: { url, _, _, message in }, openInstantPage: { message, data in - - }, longTap: { action, message in - + }, longTap: { action, message in }, getHiddenMedia: { return [:] }) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, searchQuery: "", context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, searchQuery: "", context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { }, toggleExpandGlobalResults: { }, searchPeer: { _ in diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift index 3dba623006..bd4b821ef2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift @@ -14,7 +14,7 @@ import ShareController import GalleryUI import AppBundle -private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: .white) +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) private let textFont = Font.regular(16.0) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 208a6df4a3..022b8cc8e0 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -1120,7 +1120,7 @@ [_playerItemDisposable setDisposable:[[itemSignal deliverOn:[SQueue mainQueue]] startWithNext:^(AVPlayerItem *playerItem) { __strong TGMediaPickerGalleryVideoItemView *strongSelf = weakSelf; - if (strongSelf == nil) + if (strongSelf == nil || ![playerItem isKindOfClass:[AVPlayerItem class]]) return; strongSelf->_player = [AVPlayer playerWithPlayerItem:playerItem]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m b/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m index c6f1895ed1..ad71a5b1fe 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m @@ -87,6 +87,7 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; _textView.autocorrectionType = UITextAutocorrectionTypeNo; _textView.spellCheckingType = UITextSpellCheckingTypeNo; _textView.font = [UIFont boldSystemFontOfSize:_baseFontSize]; + _textView.typingAttributes = @{NSFontAttributeName: _textView.font}; // _textView.frameWidthInset = floor(_baseFontSize * 0.03); [self setSwatch:entity.swatch]; @@ -175,6 +176,7 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; { _font = font; _textView.font = [UIFont boldSystemFontOfSize:_baseFontSize]; + _textView.typingAttributes = @{NSFontAttributeName: _textView.font}; // _textView.frameWidthInset = floor(_baseFontSize * 0.03); [self sizeToFit]; @@ -480,6 +482,9 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; @implementation TGPhotoTextView +{ + UIFont *_font; +} - (instancetype)initWithFrame:(CGRect)frame { @@ -554,10 +559,29 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; - (void)setFont:(UIFont *)font { [super setFont:font]; + _font = font; self.layoutManager.textContainers.firstObject.lineFragmentPadding = floor(font.pointSize * 0.3); } +- (void)insertText:(NSString *)text { + [self fixTypingAttributes]; + [super insertText:text]; + [self fixTypingAttributes]; +} + +- (void)paste:(id)sender { + [self fixTypingAttributes]; + [super paste:sender]; + [self fixTypingAttributes]; +} + +- (void)fixTypingAttributes { + if (_font != nil) { + self.typingAttributes = @{NSFontAttributeName: _font}; + } +} + @end @@ -802,8 +826,6 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; return [_impl attributesAtIndex:location effectiveRange:range]; } - - - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { [self beginEditing]; [_impl replaceCharactersInRange:range withString:str]; diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index e625ab6971..e40d65541f 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -89,36 +89,6 @@ private func extensionImage(fileExtension: String?) -> UIImage? { } private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold]) -func fullAuthorString(for item: ListMessageItem) -> String { - var authorString = "" - if let author = item.message.author, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(item.message.id.peerId.namespace) { - var authorName = "" - if author.id == item.context.account.peerId { - authorName = item.presentationData.strings.DialogList_You - } else { - authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - } - if let peer = item.message.peers[item.message.id.peerId], author.id != peer.id { - authorString = "\(authorName) → \(peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))" - } else { - authorString = authorName - } - } else if let peer = item.message.peers[item.message.id.peerId] { - if item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { - authorString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - } else { - if item.message.id.peerId == item.context.account.peerId { - authorString = item.presentationData.strings.DialogList_SavedMessages - } else if item.message.flags.contains(.Incoming) { - authorString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - } else { - authorString = "\(item.presentationData.strings.DialogList_You) → \(peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))" - } - } - } - return authorString -} - private struct FetchControls { let fetch: () -> Void let cancel: () -> Void @@ -418,7 +388,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } if item.isGlobalSearchResult { - let authorString = fullAuthorString(for: item) + let authorString = stringForFullAuthorName(message: item.message, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) if descriptionString.isEmpty { descriptionString = authorString } else { @@ -458,7 +428,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } if item.isGlobalSearchResult { - authorName = fullAuthorString(for: item) + authorName = stringForFullAuthorName(message: item.message, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) } titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) @@ -511,7 +481,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } if item.isGlobalSearchResult { - let authorString = fullAuthorString(for: item) + let authorString = stringForFullAuthorName(message: item.message, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) if descriptionString.isEmpty { descriptionString = authorString } else { diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index dfd0e4ef98..dc75279a93 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -11,9 +11,9 @@ import AccountContext import TelegramUIPreferences public final class ListMessageItemInteraction { - let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool - let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void - let toggleMessagesSelection: ([MessageId], Bool) -> Void + public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool + public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void + public let toggleMessagesSelection: ([MessageId], Bool) -> Void let openUrl: (String, Bool, Bool?, Message?) -> Void let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 7d4706b13b..946f8f7ecf 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -479,7 +479,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var authorString = "" if item.isGlobalSearchResult { - authorString = fullAuthorString(for: item) + authorString = stringForFullAuthorName(message: item.message, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) } let authorText = NSAttributedString(string: authorString, font: authorFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index b88a874b46..c90736413a 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -48,7 +48,7 @@ public enum LocationActionListItemIcon: Equatable { } private func generateLocationIcon(theme: PresentationTheme) -> UIImage { - return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) @@ -64,7 +64,7 @@ private func generateLocationIcon(theme: PresentationTheme) -> UIImage { } private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage { - return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(rgb: 0x6cc139).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index bed925e875..dd20679a71 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -10,7 +10,7 @@ private let panelSize = CGSize(width: 46.0, height: 90.0) private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? { let cornerRadius: CGFloat = 9.0 - return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0), contextGenerator: { size, context in + return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor) @@ -22,7 +22,7 @@ private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? { } private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> UIImage? { - return generateImage(CGSize(width: 26.0, height: 14.0), contextGenerator: { size, context in + return generateImage(CGSize(width: 26.0, height: 14.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor) diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 280f1bca0a..0b63aef373 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -15,7 +15,7 @@ import GalleryUI import AppBundle private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) -private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: .white) +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) private let nameFont = Font.medium(15.0) private let dateFont = Font.regular(14.0) diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 57d9f4ee3e..7f97005678 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -134,7 +134,7 @@ public final class SearchDisplayController { insertSubnode(self.backgroundNode, false) self.backgroundNode.addSubnode(self.contentNode) - self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.contentNode.frame = CGRect(origin: CGPoint(x: 20.0, y: 20.0), size: layout.size) self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate) @@ -149,7 +149,7 @@ public final class SearchDisplayController { let contentNodePosition = self.contentNode.layer.position // self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) self.searchBar.placeholderString = placeholder.placeholderString } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift index 1002474af6..78a1c64556 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift @@ -38,8 +38,8 @@ final class ThemeGridSelectionPanelNode: ASDisplayNode { self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) - self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) - self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) super.init() @@ -56,8 +56,8 @@ final class ThemeGridSelectionPanelNode: ASDisplayNode { self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) - self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) - self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 51e9f96999..923279b761 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -139,7 +139,7 @@ public final class ThemePreviewController: ViewController { self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) if !isPreview { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) } self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get()) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index c6abb1ff4e..97bcba4e33 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -227,7 +227,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let contentSize: CGSize let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let defaultAction = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) + let defaultAction = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) let progressAction = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: presentationData.theme.rootController.navigationBar.accentTextColor)) var isBlurrable = true diff --git a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift index dda4ebcc40..3a229ba33f 100644 --- a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift @@ -17,7 +17,7 @@ final class ShareControllerRecentPeersGridItem: GridItem { let controllerInteraction: ShareControllerInteraction let section: GridSection? = nil - let fillsRowWithHeight: (CGFloat, Bool)? = (130.0, true) + let fillsRowWithHeight: (CGFloat, Bool)? = (102.0, true) init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction) { self.context = context diff --git a/submodules/TelegramCore/Sources/SearchMessages.swift b/submodules/TelegramCore/Sources/SearchMessages.swift index 6a77448943..ce26042057 100644 --- a/submodules/TelegramCore/Sources/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/SearchMessages.swift @@ -238,7 +238,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q } else { let lowerBound = state?.main.messages.last.flatMap({ $0.index }) let signal: Signal - if peer.id.namespace == Namespaces.Peer.CloudChannel && tags == nil && minDate == nil && maxDate == nil { + if peer.id.namespace == Namespaces.Peer.CloudChannel && query.isEmpty && tags == nil && minDate == nil && maxDate == nil { signal = account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: lowerBound?.id.id ?? 0, offsetDate: 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) } else { signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputUser, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) diff --git a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift index 9ae0e58720..e75f2c45d8 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift @@ -4769,635 +4769,631 @@ public final class PresentationStrings: Equatable { public var Channel_AddBotErrorHaveRights: String { return self._s[4306]! } public var ChatList_TabIconFoldersTooltipNonEmptyFolders: String { return self._s[4307]! } public var DialogList_EncryptionProcessing: String { return self._s[4308]! } - public var WatchRemote_NotificationText: String { return self._s[4309]! } - public var EditTheme_ChangeColors: String { return self._s[4310]! } - public var GroupRemoved_ViewUserInfo: String { return self._s[4311]! } - public var Wallet_TransactionInfo_RecipientHeader: String { return self._s[4312]! } - public var CallSettings_OnMobile: String { return self._s[4314]! } - public var Month_ShortFebruary: String { return self._s[4316]! } - public var VoiceOver_MessageContextReply: String { return self._s[4317]! } + public var ChatList_Search_FilterChats: String { return self._s[4309]! } + public var WatchRemote_NotificationText: String { return self._s[4310]! } + public var EditTheme_ChangeColors: String { return self._s[4311]! } + public var GroupRemoved_ViewUserInfo: String { return self._s[4312]! } + public var Wallet_TransactionInfo_RecipientHeader: String { return self._s[4313]! } + public var CallSettings_OnMobile: String { return self._s[4315]! } + public var Month_ShortFebruary: String { return self._s[4317]! } + public var VoiceOver_MessageContextReply: String { return self._s[4318]! } public func PUSH_VIDEO_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4318]!, self._r[4318]!, [_1]) + return formatWithArgumentRanges(self._s[4319]!, self._r[4319]!, [_1]) } - public var Group_Location_ChangeLocation: String { return self._s[4319]! } - public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[4320]! } - public var Wallet_Send_EncryptComment: String { return self._s[4321]! } - public var VoiceOver_Media_PlaybackStop: String { return self._s[4322]! } - public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[4323]! } + public var Group_Location_ChangeLocation: String { return self._s[4320]! } + public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[4321]! } + public var Wallet_Send_EncryptComment: String { return self._s[4322]! } + public var VoiceOver_Media_PlaybackStop: String { return self._s[4323]! } + public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[4324]! } public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4325]!, self._r[4325]!, [_0]) + return formatWithArgumentRanges(self._s[4326]!, self._r[4326]!, [_0]) } - public var PhotoEditor_WarmthTool: String { return self._s[4326]! } - public var Login_InfoAvatarPhoto: String { return self._s[4327]! } - public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[4328]! } - public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[4329]! } - public var Map_PlacesInThisArea: String { return self._s[4330]! } - public var VoiceOver_Chat_ContactEmail: String { return self._s[4331]! } - public var Notifications_InAppNotificationsSounds: String { return self._s[4332]! } + public var PhotoEditor_WarmthTool: String { return self._s[4327]! } + public var Login_InfoAvatarPhoto: String { return self._s[4328]! } + public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[4329]! } + public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[4330]! } + public var Map_PlacesInThisArea: String { return self._s[4331]! } + public var VoiceOver_Chat_ContactEmail: String { return self._s[4332]! } + public var Notifications_InAppNotificationsSounds: String { return self._s[4333]! } public func PUSH_PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4333]!, self._r[4333]!, [_1]) + return formatWithArgumentRanges(self._s[4334]!, self._r[4334]!, [_1]) } - public var ShareMenu_Send: String { return self._s[4334]! } - public var Username_InvalidStartsWithNumber: String { return self._s[4335]! } - public var Appearance_AppIconClassicX: String { return self._s[4336]! } + public var ShareMenu_Send: String { return self._s[4335]! } + public var Username_InvalidStartsWithNumber: String { return self._s[4336]! } + public var Appearance_AppIconClassicX: String { return self._s[4337]! } public func PUSH_CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4337]!, self._r[4337]!, [_1]) + return formatWithArgumentRanges(self._s[4338]!, self._r[4338]!, [_1]) } - public var Conversation_StopPoll: String { return self._s[4338]! } - public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[4340]! } - public var Passport_Identity_EditIdentityCard: String { return self._s[4341]! } - public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[4342]! } - public var Wallet_WordCheck_Title: String { return self._s[4343]! } - public var Conversation_Timer_Title: String { return self._s[4344]! } - public var Common_Next: String { return self._s[4345]! } - public var Notification_Exceptions_NewException: String { return self._s[4346]! } + public var Conversation_StopPoll: String { return self._s[4339]! } + public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[4341]! } + public var Passport_Identity_EditIdentityCard: String { return self._s[4342]! } + public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[4343]! } + public var Wallet_WordCheck_Title: String { return self._s[4344]! } + public var Conversation_Timer_Title: String { return self._s[4345]! } + public var Common_Next: String { return self._s[4346]! } + public var Notification_Exceptions_NewException: String { return self._s[4347]! } public func Generic_OpenHiddenLinkAlert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4347]!, self._r[4347]!, [_0]) + return formatWithArgumentRanges(self._s[4348]!, self._r[4348]!, [_0]) } - public var AccessDenied_CallMicrophone: String { return self._s[4348]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[4349]! } - public var ChangePhoneNumberCode_Help: String { return self._s[4350]! } - public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[4351]! } - public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[4352]! } - public var BlockedUsers_LeavePrefix: String { return self._s[4353]! } + public var AccessDenied_CallMicrophone: String { return self._s[4349]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[4350]! } + public var ChangePhoneNumberCode_Help: String { return self._s[4351]! } + public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[4352]! } + public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[4353]! } + public var BlockedUsers_LeavePrefix: String { return self._s[4354]! } public func Passport_RequestHeader(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4354]!, self._r[4354]!, [_0]) + return formatWithArgumentRanges(self._s[4355]!, self._r[4355]!, [_0]) } - public var Group_About_Help: String { return self._s[4355]! } - public var TwoStepAuth_ChangePasswordDescription: String { return self._s[4356]! } - public var Tour_Title3: String { return self._s[4357]! } - public var Watch_Conversation_Unblock: String { return self._s[4358]! } - public var Watch_UserInfo_Block: String { return self._s[4359]! } - public var Notifications_ChannelNotificationsAlert: String { return self._s[4360]! } - public var TwoFactorSetup_Hint_Action: String { return self._s[4361]! } - public var IntentsSettings_SuggestedChatsInfo: String { return self._s[4362]! } - public var Wallet_Alert_Cancel: String { return self._s[4363]! } - public var TextFormat_AddLinkTitle: String { return self._s[4364]! } - public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[4365]! } - public var TwoStepAuth_EnterPasswordTitle: String { return self._s[4366]! } - public var FastTwoStepSetup_PasswordSection: String { return self._s[4367]! } - public var Compose_ChannelMembers: String { return self._s[4368]! } - public var Conversation_ForwardTitle: String { return self._s[4369]! } + public var Group_About_Help: String { return self._s[4356]! } + public var TwoStepAuth_ChangePasswordDescription: String { return self._s[4357]! } + public var Tour_Title3: String { return self._s[4358]! } + public var Watch_Conversation_Unblock: String { return self._s[4359]! } + public var Watch_UserInfo_Block: String { return self._s[4360]! } + public var Notifications_ChannelNotificationsAlert: String { return self._s[4361]! } + public var TwoFactorSetup_Hint_Action: String { return self._s[4362]! } + public var IntentsSettings_SuggestedChatsInfo: String { return self._s[4363]! } + public var Wallet_Alert_Cancel: String { return self._s[4364]! } + public var TextFormat_AddLinkTitle: String { return self._s[4365]! } + public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[4366]! } + public var TwoStepAuth_EnterPasswordTitle: String { return self._s[4367]! } + public var FastTwoStepSetup_PasswordSection: String { return self._s[4368]! } + public var Compose_ChannelMembers: String { return self._s[4369]! } + public var Conversation_ForwardTitle: String { return self._s[4370]! } public func Wallet_Updated_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4370]!, self._r[4370]!, [_0]) + return formatWithArgumentRanges(self._s[4371]!, self._r[4371]!, [_0]) } - public var Conversation_PinnedPoll: String { return self._s[4372]! } + public var Conversation_PinnedPoll: String { return self._s[4373]! } public func VoiceOver_Chat_AnonymousPollFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4373]!, self._r[4373]!, [_0]) + return formatWithArgumentRanges(self._s[4374]!, self._r[4374]!, [_0]) } - public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[4374]! } - public var Conversation_ContextMenuStickerPackAdd: String { return self._s[4375]! } - public var Stats_Overview: String { return self._s[4376]! } - public var Map_HomeAndWorkTitle: String { return self._s[4377]! } - public var Wallet_Intro_Terms: String { return self._s[4378]! } + public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[4375]! } + public var Conversation_ContextMenuStickerPackAdd: String { return self._s[4376]! } + public var Stats_Overview: String { return self._s[4377]! } + public var Map_HomeAndWorkTitle: String { return self._s[4378]! } + public var Wallet_Intro_Terms: String { return self._s[4379]! } public func Time_PreciseDate_m4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4379]!, self._r[4379]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4380]!, self._r[4380]!, [_1, _2, _3]) } - public var Passport_Address_CityPlaceholder: String { return self._s[4380]! } - public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[4381]! } - public var Privacy_PhoneNumber: String { return self._s[4382]! } - public var ChatList_Search_FilterFiles: String { return self._s[4383]! } - public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[4384]! } - public var ChannelIntro_CreateChannel: String { return self._s[4385]! } - public var Conversation_InputTextAnonymousPlaceholder: String { return self._s[4386]! } + public var Passport_Address_CityPlaceholder: String { return self._s[4381]! } + public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[4382]! } + public var Privacy_PhoneNumber: String { return self._s[4383]! } + public var ChatList_Search_FilterFiles: String { return self._s[4384]! } + public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[4385]! } + public var ChannelIntro_CreateChannel: String { return self._s[4386]! } + public var Conversation_InputTextAnonymousPlaceholder: String { return self._s[4387]! } public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4387]!, self._r[4387]!, [_0]) + return formatWithArgumentRanges(self._s[4388]!, self._r[4388]!, [_0]) } - public var Weekday_ShortMonday: String { return self._s[4388]! } - public var Passport_Language_ar: String { return self._s[4390]! } - public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[4391]! } - public var TwoFactorSetup_Done_Title: String { return self._s[4392]! } - public var Calls_RatingFeedback: String { return self._s[4393]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[4394]! } - public var AutoDownloadSettings_ResetSettings: String { return self._s[4397]! } - public var Watch_Compose_Send: String { return self._s[4398]! } - public var PasscodeSettings_ChangePasscode: String { return self._s[4399]! } - public var WebSearch_RecentSectionClear: String { return self._s[4400]! } + public var Weekday_ShortMonday: String { return self._s[4389]! } + public var Passport_Language_ar: String { return self._s[4391]! } + public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[4392]! } + public var TwoFactorSetup_Done_Title: String { return self._s[4393]! } + public var Calls_RatingFeedback: String { return self._s[4394]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[4395]! } + public var AutoDownloadSettings_ResetSettings: String { return self._s[4398]! } + public var Watch_Compose_Send: String { return self._s[4399]! } + public var PasscodeSettings_ChangePasscode: String { return self._s[4400]! } + public var WebSearch_RecentSectionClear: String { return self._s[4401]! } public func Contacts_AccessDeniedHelpPortrait(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4401]!, self._r[4401]!, [_0]) + return formatWithArgumentRanges(self._s[4402]!, self._r[4402]!, [_0]) } - public var WallpaperSearch_ColorTeal: String { return self._s[4402]! } - public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[4403]! } - public var Permissions_ContactsTitle_v0: String { return self._s[4404]! } - public var Checkout_PasswordEntry_Pay: String { return self._s[4406]! } - public var Settings_SavedMessages: String { return self._s[4407]! } - public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[4408]! } - public var Month_ShortMarch: String { return self._s[4409]! } - public var Message_Location: String { return self._s[4410]! } + public var WallpaperSearch_ColorTeal: String { return self._s[4403]! } + public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[4404]! } + public var Permissions_ContactsTitle_v0: String { return self._s[4405]! } + public var Checkout_PasswordEntry_Pay: String { return self._s[4407]! } + public var Settings_SavedMessages: String { return self._s[4408]! } + public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[4409]! } + public var Month_ShortMarch: String { return self._s[4410]! } + public var Message_Location: String { return self._s[4411]! } public func PUSH_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4411]!, self._r[4411]!, [_1]) + return formatWithArgumentRanges(self._s[4412]!, self._r[4412]!, [_1]) } public func Notification_CallTimeFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4412]!, self._r[4412]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4413]!, self._r[4413]!, [_1, _2]) } - public var VoiceOver_Chat_VoiceMessage: String { return self._s[4414]! } + public var VoiceOver_Chat_VoiceMessage: String { return self._s[4415]! } public func Channel_AdminLog_MessageChangedUnlinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4415]!, self._r[4415]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4416]!, self._r[4416]!, [_1, _2]) } - public var GroupPermission_NoSendMedia: String { return self._s[4416]! } - public var Conversation_ClousStorageInfo_Description2: String { return self._s[4417]! } - public var SharedMedia_CategoryDocs: String { return self._s[4418]! } - public var Appearance_RemoveThemeConfirmation: String { return self._s[4419]! } - public var Paint_Framed: String { return self._s[4420]! } - public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[4421]! } - public var Passport_Identity_DoesNotExpire: String { return self._s[4422]! } - public var Channel_SignMessages: String { return self._s[4423]! } - public var Contacts_AccessDeniedHelpON: String { return self._s[4424]! } - public var Conversation_ContextMenuStickerPackInfo: String { return self._s[4425]! } + public var GroupPermission_NoSendMedia: String { return self._s[4417]! } + public var Conversation_ClousStorageInfo_Description2: String { return self._s[4418]! } + public var SharedMedia_CategoryDocs: String { return self._s[4419]! } + public var Appearance_RemoveThemeConfirmation: String { return self._s[4420]! } + public var Paint_Framed: String { return self._s[4421]! } + public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[4422]! } + public var Passport_Identity_DoesNotExpire: String { return self._s[4423]! } + public var Channel_SignMessages: String { return self._s[4424]! } + public var Contacts_AccessDeniedHelpON: String { return self._s[4425]! } + public var Conversation_ContextMenuStickerPackInfo: String { return self._s[4426]! } public func PUSH_CHAT_LEFT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4426]!, self._r[4426]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4427]!, self._r[4427]!, [_1, _2]) } - public var GroupInfo_UpgradeButton: String { return self._s[4427]! } - public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[4428]! } - public var AutoDownloadSettings_Files: String { return self._s[4429]! } + public var GroupInfo_UpgradeButton: String { return self._s[4428]! } + public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[4429]! } + public var AutoDownloadSettings_Files: String { return self._s[4430]! } public func Notification_ChangedGroupName(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4430]!, self._r[4430]!, [_0, _1]) + return formatWithArgumentRanges(self._s[4431]!, self._r[4431]!, [_0, _1]) } - public var Login_SendCodeViaSms: String { return self._s[4432]! } - public var Update_UpdateApp: String { return self._s[4433]! } - public var Channel_Setup_TypePublic: String { return self._s[4434]! } - public var Watch_Compose_CreateMessage: String { return self._s[4435]! } + public var Login_SendCodeViaSms: String { return self._s[4433]! } + public var Update_UpdateApp: String { return self._s[4434]! } + public var Channel_Setup_TypePublic: String { return self._s[4435]! } + public var Watch_Compose_CreateMessage: String { return self._s[4436]! } public func PUSH_CHAT_MESSAGE_VIDEOS(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4436]!, self._r[4436]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4437]!, self._r[4437]!, [_1, _2, _3]) } - public var StickerPacksSettings_ManagingHelp: String { return self._s[4437]! } + public var StickerPacksSettings_ManagingHelp: String { return self._s[4438]! } public func Wallet_Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4438]!, self._r[4438]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4439]!, self._r[4439]!, [_1, _2, _3]) } - public var VoiceOver_Chat_Video: String { return self._s[4439]! } - public var Forward_ChannelReadOnly: String { return self._s[4440]! } - public var StickerPack_HideStickers: String { return self._s[4441]! } - public var ChatListFolder_NameContacts: String { return self._s[4442]! } - public var Profile_BotInfo: String { return self._s[4443]! } - public var Document_TargetConfirmationFormat: String { return self._s[4444]! } - public var GroupInfo_InviteByLink: String { return self._s[4445]! } - public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[4446]! } - public var Watch_Stickers_RecentPlaceholder: String { return self._s[4447]! } - public var Broadcast_AdminLog_EmptyText: String { return self._s[4448]! } - public var Passport_NotLoggedInMessage: String { return self._s[4449]! } - public var Conversation_StopQuizConfirmation: String { return self._s[4450]! } - public var Checkout_PaymentMethod: String { return self._s[4451]! } - public var ChatList_ArchivedChatsTitle: String { return self._s[4455]! } - public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[4456]! } - public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[4457]! } - public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[4458]! } - public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[4459]! } - public var Camera_Title: String { return self._s[4460]! } - public var Map_Directions: String { return self._s[4461]! } - public var Wallet_Intro_ImportExisting: String { return self._s[4462]! } - public var Stats_MessagePublicForwardsTitle: String { return self._s[4463]! } - public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[4465]! } - public var Profile_EncryptionKey: String { return self._s[4466]! } + public var VoiceOver_Chat_Video: String { return self._s[4440]! } + public var Forward_ChannelReadOnly: String { return self._s[4441]! } + public var StickerPack_HideStickers: String { return self._s[4442]! } + public var ChatListFolder_NameContacts: String { return self._s[4443]! } + public var Profile_BotInfo: String { return self._s[4444]! } + public var Document_TargetConfirmationFormat: String { return self._s[4445]! } + public var GroupInfo_InviteByLink: String { return self._s[4446]! } + public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[4447]! } + public var Watch_Stickers_RecentPlaceholder: String { return self._s[4448]! } + public var Broadcast_AdminLog_EmptyText: String { return self._s[4449]! } + public var Passport_NotLoggedInMessage: String { return self._s[4450]! } + public var Conversation_StopQuizConfirmation: String { return self._s[4451]! } + public var Checkout_PaymentMethod: String { return self._s[4452]! } + public var ChatList_ArchivedChatsTitle: String { return self._s[4456]! } + public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[4457]! } + public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[4458]! } + public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[4459]! } + public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[4460]! } + public var Camera_Title: String { return self._s[4461]! } + public var Map_Directions: String { return self._s[4462]! } + public var Wallet_Intro_ImportExisting: String { return self._s[4463]! } + public var Stats_MessagePublicForwardsTitle: String { return self._s[4464]! } + public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[4466]! } + public var Profile_EncryptionKey: String { return self._s[4467]! } public func LOCAL_CHAT_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4467]!, self._r[4467]!, [_1, "\(_2)"]) + return formatWithArgumentRanges(self._s[4468]!, self._r[4468]!, [_1, "\(_2)"]) } public func Compatibility_SecretMediaVersionTooLow(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4468]!, self._r[4468]!, [_0, _1]) + return formatWithArgumentRanges(self._s[4469]!, self._r[4469]!, [_0, _1]) } - public var Passport_Identity_TypePassport: String { return self._s[4469]! } - public var CreatePoll_QuizOptionsHeader: String { return self._s[4471]! } - public var Common_No: String { return self._s[4472]! } - public var Conversation_SendMessage_ScheduleMessage: String { return self._s[4473]! } - public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[4474]! } - public var Settings_AboutEmpty: String { return self._s[4475]! } - public var TwoStepAuth_FloodError: String { return self._s[4477]! } - public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[4478]! } + public var Passport_Identity_TypePassport: String { return self._s[4470]! } + public var CreatePoll_QuizOptionsHeader: String { return self._s[4472]! } + public var Common_No: String { return self._s[4473]! } + public var Conversation_SendMessage_ScheduleMessage: String { return self._s[4474]! } + public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[4475]! } + public var Settings_AboutEmpty: String { return self._s[4476]! } + public var TwoStepAuth_FloodError: String { return self._s[4478]! } + public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[4479]! } public func Channel_AdminLog_MessageUnkickedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4480]!, self._r[4480]!, [_1]) + return formatWithArgumentRanges(self._s[4481]!, self._r[4481]!, [_1]) } - public var Conversation_Edit: String { return self._s[4483]! } - public var CheckoutInfo_SaveInfo: String { return self._s[4484]! } - public var VoiceOver_Chat_AnonymousPoll: String { return self._s[4485]! } - public var Call_CameraTooltip: String { return self._s[4487]! } - public var InstantPage_FeedbackButtonShort: String { return self._s[4488]! } - public var Contacts_InviteToTelegram: String { return self._s[4489]! } - public var Wallet_WordImport_CanNotRemember: String { return self._s[4490]! } - public var Notifications_ResetAllNotifications: String { return self._s[4491]! } - public var Calls_NewCall: String { return self._s[4492]! } - public var VoiceOver_Chat_Music: String { return self._s[4495]! } - public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[4496]! } - public var Channel_Edit_AboutItem: String { return self._s[4497]! } - public var Message_VideoExpired: String { return self._s[4498]! } - public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[4499]! } + public var Conversation_Edit: String { return self._s[4484]! } + public var CheckoutInfo_SaveInfo: String { return self._s[4485]! } + public var VoiceOver_Chat_AnonymousPoll: String { return self._s[4486]! } + public var Call_CameraTooltip: String { return self._s[4488]! } + public var InstantPage_FeedbackButtonShort: String { return self._s[4489]! } + public var Contacts_InviteToTelegram: String { return self._s[4490]! } + public var Wallet_WordImport_CanNotRemember: String { return self._s[4491]! } + public var Notifications_ResetAllNotifications: String { return self._s[4492]! } + public var Calls_NewCall: String { return self._s[4493]! } + public var VoiceOver_Chat_Music: String { return self._s[4496]! } + public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[4497]! } + public var Channel_Edit_AboutItem: String { return self._s[4498]! } + public var Message_VideoExpired: String { return self._s[4499]! } + public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[4500]! } public func PUSH_CHAT_RETURNED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4500]!, self._r[4500]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4501]!, self._r[4501]!, [_1, _2]) } - public var NotificationsSound_Input: String { return self._s[4502]! } - public var Notifications_ClassicTones: String { return self._s[4503]! } - public var Conversation_StatusTyping: String { return self._s[4504]! } - public var Checkout_ErrorProviderAccountInvalid: String { return self._s[4505]! } - public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[4506]! } - public var Wallet_Month_ShortSeptember: String { return self._s[4507]! } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[4508]! } - public var Conversation_MessageLeaveComment: String { return self._s[4509]! } - public var UserInfo_TapToCall: String { return self._s[4510]! } - public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[4511]! } - public var Conversation_ClearAll: String { return self._s[4513]! } - public var UserInfo_NotificationsDefault: String { return self._s[4514]! } - public var Wallet_Send_OwnAddressAlertText: String { return self._s[4515]! } - public var Map_ChooseAPlace: String { return self._s[4516]! } + public var NotificationsSound_Input: String { return self._s[4503]! } + public var Notifications_ClassicTones: String { return self._s[4504]! } + public var Conversation_StatusTyping: String { return self._s[4505]! } + public var Checkout_ErrorProviderAccountInvalid: String { return self._s[4506]! } + public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[4507]! } + public var Wallet_Month_ShortSeptember: String { return self._s[4508]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[4509]! } + public var Conversation_MessageLeaveComment: String { return self._s[4510]! } + public var UserInfo_TapToCall: String { return self._s[4511]! } + public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[4512]! } + public var Conversation_ClearAll: String { return self._s[4514]! } + public var UserInfo_NotificationsDefault: String { return self._s[4515]! } + public var Wallet_Send_OwnAddressAlertText: String { return self._s[4516]! } + public var Map_ChooseAPlace: String { return self._s[4517]! } public func Wallet_Receive_ShareInvoiceUrlInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4517]!, self._r[4517]!, [_0]) + return formatWithArgumentRanges(self._s[4518]!, self._r[4518]!, [_0]) } - public var GroupInfo_AddParticipantTitle: String { return self._s[4518]! } - public var ChatList_PeerTypeNonContact: String { return self._s[4519]! } - public var Conversation_SlideToCancel: String { return self._s[4520]! } - public var Month_ShortJuly: String { return self._s[4521]! } - public var SocksProxySetup_ProxyType: String { return self._s[4522]! } + public var GroupInfo_AddParticipantTitle: String { return self._s[4519]! } + public var ChatList_PeerTypeNonContact: String { return self._s[4520]! } + public var Conversation_SlideToCancel: String { return self._s[4521]! } + public var Month_ShortJuly: String { return self._s[4522]! } + public var SocksProxySetup_ProxyType: String { return self._s[4523]! } public func ChatList_DeleteChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4523]!, self._r[4523]!, [_0]) + return formatWithArgumentRanges(self._s[4524]!, self._r[4524]!, [_0]) } - public var ChatList_EditFolders: String { return self._s[4524]! } - public var TwoStepAuth_SetPasswordHelp: String { return self._s[4525]! } - public var Wallet_Send_ConfirmationConfirm: String { return self._s[4527]! } - public var Wallet_Created_ExportErrorTitle: String { return self._s[4528]! } + public var ChatList_EditFolders: String { return self._s[4525]! } + public var TwoStepAuth_SetPasswordHelp: String { return self._s[4526]! } + public var Wallet_Send_ConfirmationConfirm: String { return self._s[4528]! } + public var Wallet_Created_ExportErrorTitle: String { return self._s[4529]! } public func GroupPermission_ApplyAlertText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4529]!, self._r[4529]!, [_0]) + return formatWithArgumentRanges(self._s[4530]!, self._r[4530]!, [_0]) } - public var Permissions_PeopleNearbyTitle_v0: String { return self._s[4530]! } - public var ScheduledMessages_RemindersTitle: String { return self._s[4531]! } - public var Your_cards_expiration_year_is_invalid: String { return self._s[4532]! } - public var Wallet_Info_TransactionPendingHeader: String { return self._s[4534]! } - public var UserInfo_ShareMyContactInfo: String { return self._s[4535]! } - public var Passport_DeleteAddress: String { return self._s[4537]! } - public var Passport_DeletePassportConfirmation: String { return self._s[4538]! } - public var Passport_Identity_ReverseSide: String { return self._s[4539]! } - public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[4540]! } - public var Login_InfoLastNamePlaceholder: String { return self._s[4541]! } - public var Passport_FieldAddress: String { return self._s[4542]! } - public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[4543]! } - public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[4545]! } - public var Map_Home: String { return self._s[4547]! } - public var PollResults_Title: String { return self._s[4548]! } - public var ArchivedChats_IntroText2: String { return self._s[4550]! } - public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[4551]! } - public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[4552]! } - public var CallFeedback_ReasonSilentRemote: String { return self._s[4554]! } - public var Passport_Identity_AddPersonalDetails: String { return self._s[4556]! } - public var Group_Info_AdminLog: String { return self._s[4558]! } - public var ChatSettings_AutoPlayTitle: String { return self._s[4559]! } - public var Appearance_Animations: String { return self._s[4560]! } - public var Appearance_TextSizeSetting: String { return self._s[4561]! } - public func MessageTimer_Minutes(_ value: Int32) -> String { + public var Permissions_PeopleNearbyTitle_v0: String { return self._s[4531]! } + public var ScheduledMessages_RemindersTitle: String { return self._s[4532]! } + public var Your_cards_expiration_year_is_invalid: String { return self._s[4533]! } + public var Wallet_Info_TransactionPendingHeader: String { return self._s[4535]! } + public var UserInfo_ShareMyContactInfo: String { return self._s[4536]! } + public var Passport_DeleteAddress: String { return self._s[4538]! } + public var Passport_DeletePassportConfirmation: String { return self._s[4539]! } + public var Passport_Identity_ReverseSide: String { return self._s[4540]! } + public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[4541]! } + public var Login_InfoLastNamePlaceholder: String { return self._s[4542]! } + public var Passport_FieldAddress: String { return self._s[4543]! } + public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[4544]! } + public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[4546]! } + public var Map_Home: String { return self._s[4548]! } + public var PollResults_Title: String { return self._s[4549]! } + public var ArchivedChats_IntroText2: String { return self._s[4551]! } + public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[4552]! } + public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[4553]! } + public var CallFeedback_ReasonSilentRemote: String { return self._s[4555]! } + public var Passport_Identity_AddPersonalDetails: String { return self._s[4557]! } + public var Group_Info_AdminLog: String { return self._s[4559]! } + public var ChatSettings_AutoPlayTitle: String { return self._s[4560]! } + public var Appearance_Animations: String { return self._s[4561]! } + public var Appearance_TextSizeSetting: String { return self._s[4562]! } + public func OldChannels_Leave(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_ShortHours(_ value: Int32) -> String { + public func ChatList_MessagePhotos(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_Months(_ value: Int32) -> String { + public func VoiceOver_Chat_ContactPhoneNumberCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[2 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notifications_ExceptionMuteExpires_Days(_ value: Int32) -> String { + public func MuteExpires_Minutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[3 * 6 + Int(form.rawValue)]!, stringValue) } - public func OldChannels_Leave(_ value: Int32) -> String { + public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[4 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[5 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Contacts_ImportersCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[6 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_TitleComments(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[7 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[8 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[9 * 6 + Int(form.rawValue)]!, stringValue) + public func ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[5 * 6 + Int(form.rawValue)]!, _0, _1) } public func ChatListFilter_ShowMoreChats(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[10 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedVideos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[11 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Invitation_Members(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[12 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_ShortSeconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[13 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessagePoll_VotedCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[14 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedGifs(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[15 * 6 + Int(form.rawValue)]!, stringValue) - } - public func OldChannels_InactiveYear(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[16 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[17 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[19 * 6 + Int(form.rawValue)]!, stringValue) - } - public func InviteText_ContactsCountText(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[20 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Map_ETAHours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[21 * 6 + Int(form.rawValue)]!, stringValue) - } - public func LastSeen_MinutesAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[22 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Link(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[23 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_Seconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[24 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_ShortMinutes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[25 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[26 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func PUSH_CHAT_MESSAGE_ROUNDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[27 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func OldChannels_InactiveWeek(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[28 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Years(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[29 * 6 + Int(form.rawValue)]!, stringValue) - } - public func VoiceOver_Chat_ContactPhoneNumberCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[30 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Weeks(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[31 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[32 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func MessageTimer_ShortWeeks(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[33 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_File(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[34 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_SelectedMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[35 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Wallpaper_DeleteConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[36 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notifications_Exceptions(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[37 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[6 * 6 + Int(form.rawValue)]!, stringValue) } public func Contacts_InviteContacts(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[38 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[7 * 6 + Int(form.rawValue)]!, stringValue) } - public func ChatList_SelectedChats(_ value: Int32) -> String { + public func CreatePoll_AddMoreOptions(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[39 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[8 * 6 + Int(form.rawValue)]!, stringValue) } - public func StickerPack_StickerCount(_ value: Int32) -> String { + public func MessageTimer_Weeks(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[40 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[9 * 6 + Int(form.rawValue)]!, stringValue) } - public func OldChannels_GroupFormat(_ value: Int32) -> String { + public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[41 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Video(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[42 * 6 + Int(form.rawValue)]!, stringValue) - } - public func OldChannels_InactiveMonth(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[43 * 6 + Int(form.rawValue)]!, stringValue) - } - public func UserCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[44 * 6 + Int(form.rawValue)]!, stringValue) - } - public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[45 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[46 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ChatList_DeleteConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[47 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PeopleNearby_ShowMorePeople(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[48 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notification_GameScoreSimple(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[49 * 6 + Int(form.rawValue)]!, stringValue) - } - public func VoiceOver_Chat_PollVotes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[50 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupTopPosterMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[51 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteExpires_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[52 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notifications_ExceptionMuteExpires_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[53 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Generic(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[54 * 6 + Int(form.rawValue)]!, stringValue) - } - public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[55 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[56 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Media_ShareItem(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[57 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ChatList_MessagePhotos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[58 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Chat_DeleteMessagesConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[59 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_AddStickerCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[60 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupTopPosterChars(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[61 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[62 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Stats_MessageViews(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[63 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[64 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedFiles(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[65 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[10 * 6 + Int(form.rawValue)]!, stringValue) } public func Conversation_ContextViewReplies(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[11 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + public func ChatList_MessageVideos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[12 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_Video(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[13 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[67 * 6 + Int(form.rawValue)]!, _1, _2) + return String(format: self._ps[14 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[15 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Call_ShortSeconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[16 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notifications_Exceptions(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[17 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGES(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func MuteExpires_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[19 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[20 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[21 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func MessageTimer_Years(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[22 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGE_VIDEOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[23 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func SharedMedia_Link(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[24 * 6 + Int(form.rawValue)]!, stringValue) } public func Call_Minutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[68 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[25 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_Seconds(_ value: Int32) -> String { + public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[69 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[26 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[27 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedVideoMessages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[28 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Months(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[29 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Invitation_Members(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[30 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[31 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_Search_Messages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[32 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[33 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notifications_ExceptionMuteExpires_Minutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[34 * 6 + Int(form.rawValue)]!, stringValue) + } + public func OldChannels_GroupFormat(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[35 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[36 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Notification_GameScoreExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[37 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortWeeks(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[38 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[39 * 6 + Int(form.rawValue)]!, stringValue) + } + public func QuickSend_Photos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[40 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Call_Seconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[41 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupShowMoreTopAdmins(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[42 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_StickerCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[43 * 6 + Int(form.rawValue)]!, stringValue) + } + public func OldChannels_InactiveMonth(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[44 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGE_PHOTOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[45 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[46 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupTopAdminBans(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[47 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[48 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupShowMoreTopInviters(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[49 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[50 * 6 + Int(form.rawValue)]!, stringValue) } public func MuteFor_Days(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[70 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[51 * 6 + Int(form.rawValue)]!, stringValue) } - public func Stats_GroupTopInviterInvites(_ value: Int32) -> String { + public func Conversation_SelectedMessages(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[71 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[52 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessagePoll_QuizCount(_ value: Int32) -> String { + public func ChatList_DeletedChats(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[53 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[54 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[55 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func OldChannels_InactiveYear(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[56 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_StatusMembers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[57 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MuteExpires_Days(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[58 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Call_ShortMinutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[59 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notifications_ExceptionMuteExpires_Days(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[60 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGE_ROUNDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[61 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func Stats_MessageForwards(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[62 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MuteFor_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[63 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[64 * 6 + Int(form.rawValue)]!, stringValue) + } + public func AttachmentMenu_SendVideo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[65 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_TitleComments(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[67 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notification_GameScoreSimple(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[68 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupTopAdminKicks(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[69 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_File(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[70 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[71 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Passport_Scans(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[72 * 6 + Int(form.rawValue)]!, stringValue) } - public func VoiceOver_Chat_ContactEmailCount(_ value: Int32) -> String { + public func AttachmentMenu_SendGif(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[73 * 6 + Int(form.rawValue)]!, stringValue) @@ -5407,347 +5403,352 @@ public final class PresentationStrings: Equatable { let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[74 * 6 + Int(form.rawValue)]!, stringValue) } - public func AttachmentMenu_SendItem(_ value: Int32) -> String { + public func StickerPack_AddStickerCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[75 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[76 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Stats_GroupTopAdminDeletions(_ value: Int32) -> String { + public func MessageTimer_ShortDays(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[77 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[78 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendVideo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[79 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHAT_MESSAGE_VIDEOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[80 * 6 + Int(form.rawValue)]!, _2, _1, _3) + return String(format: self._ps[76 * 6 + Int(form.rawValue)]!, stringValue) } public func MessageTimer_ShortMinutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[81 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[77 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedStickers(_ value: Int32) -> String { + public func Wallpaper_DeleteConfirmation(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[82 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[78 * 6 + Int(form.rawValue)]!, stringValue) } - public func Conversation_MessageViewComments(_ value: Int32) -> String { + public func Stats_GroupTopInviterInvites(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[83 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[79 * 6 + Int(form.rawValue)]!, stringValue) } - public func MuteExpires_Minutes(_ value: Int32) -> String { + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[84 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notifications_ExceptionMuteExpires_Minutes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[85 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedPhotos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[86 * 6 + Int(form.rawValue)]!, stringValue) - } - public func QuickSend_Photos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[87 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ChatList_MessageVideos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[88 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendGif(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[89 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[90 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func PollResults_ShowMore(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[91 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHAT_MESSAGE_PHOTOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[92 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func LastSeen_HoursAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[93 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ChatList_DeletedChats(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[94 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Media_SharePhoto(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[95 * 6 + Int(form.rawValue)]!, stringValue) - } - public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[96 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[97 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupTopAdminKicks(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[98 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Passport_Scans(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[99 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Theme_UsersCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[100 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedPolls(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[101 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[102 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Stats_GroupShowMoreTopAdmins(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[103 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedLocations(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[104 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_AddMaskCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[105 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupShowMoreTopPosters(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[106 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[107 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_MessageForwards(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[108 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[109 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHAT_MESSAGES(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[110 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func Conversation_StatusOnline(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[111 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupShowMoreTopInviters(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[112 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notification_GameScoreExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[113 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupTopAdminBans(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[114 * 6 + Int(form.rawValue)]!, stringValue) - } - public func VoiceOver_Chat_PollOptionCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[115 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedVideoMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[116 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Map_ETAMinutes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[117 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_StatusMembers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[118 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Photo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[119 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[120 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[121 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[122 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_StatusSubscribers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[123 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[124 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteFor_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[125 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedAudios(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[126 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Media_ShareVideo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[127 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[128 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[129 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[130 * 6 + Int(form.rawValue)]!, _0, _1) - } - public func PUSH_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[131 * 6 + Int(form.rawValue)]!, _1, _2) + return String(format: self._ps[80 * 6 + Int(form.rawValue)]!, stringValue) } public func GroupInfo_ShowMoreMembers(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[132 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[81 * 6 + Int(form.rawValue)]!, stringValue) } - public func MuteExpires_Hours(_ value: Int32) -> String { + public func AttachmentMenu_SendItem(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[133 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[82 * 6 + Int(form.rawValue)]!, stringValue) } - public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + public func PollResults_ShowMore(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[134 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[83 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedGifs(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[84 * 6 + Int(form.rawValue)]!, stringValue) } public func ForwardedContacts(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[135 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[85 * 6 + Int(form.rawValue)]!, stringValue) } - public func Watch_UserInfo_Mute(_ value: Int32) -> String { + public func Contacts_ImportersCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[136 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[86 * 6 + Int(form.rawValue)]!, stringValue) } - public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + public func PUSH_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[87 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func PUSH_CHAT_MESSAGE_FWDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[88 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func Media_ShareItem(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[137 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[89 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_MessageViewComments(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[90 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedVideos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[91 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupShowMoreTopPosters(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[92 * 6 + Int(form.rawValue)]!, stringValue) } public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[138 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[93 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_ShortDays(_ value: Int32) -> String { + public func Stats_GroupTopAdminDeletions(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[139 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[94 * 6 + Int(form.rawValue)]!, stringValue) } - public func ChatList_Search_Messages(_ value: Int32) -> String { + public func SharedMedia_Photo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[140 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[95 * 6 + Int(form.rawValue)]!, stringValue) } - public func CreatePoll_AddMoreOptions(_ value: Int32) -> String { + public func VoiceOver_Chat_ContactEmailCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[141 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[96 * 6 + Int(form.rawValue)]!, stringValue) } - public func Call_ShortSeconds(_ value: Int32) -> String { + public func VoiceOver_Chat_PollVotes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[142 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[97 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHAT_MESSAGE_FWDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + public func Map_ETAMinutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[98 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notifications_ExceptionMuteExpires_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[99 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_MessageViews(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[100 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Media_SharePhoto(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[101 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Minutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[102 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[103 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_AddMaskCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[104 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_StatusOnline(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[105 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[143 * 6 + Int(form.rawValue)]!, _2, _1, _3) + return String(format: self._ps[106 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func UserCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[107 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedPhotos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[108 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PeopleNearby_ShowMorePeople(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[109 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_StatusSubscribers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[110 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LastSeen_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[111 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Chat_DeleteMessagesConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[112 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_DeleteConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[113 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedStickers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[114 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Theme_UsersCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[115 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LastSeen_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[116 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Media_ShareVideo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[117 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessagePoll_VotedCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[118 * 6 + Int(form.rawValue)]!, stringValue) + } + public func InstantPage_Views(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[119 * 6 + Int(form.rawValue)]!, stringValue) } public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[120 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[121 * 6 + Int(form.rawValue)]!, stringValue) + } + public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[122 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedFiles(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[123 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupTopPosterChars(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[124 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[125 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_SelectedChats(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[126 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedAudios(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[127 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Seconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[128 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[129 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func ForwardedLocations(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[130 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[131 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_Generic(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[132 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Watch_UserInfo_Mute(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[133 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessagePoll_QuizCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[134 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Days(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[135 * 6 + Int(form.rawValue)]!, stringValue) + } + public func VoiceOver_Chat_PollOptionCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[136 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[137 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedPolls(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[138 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Map_ETAHours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[139 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedMessages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[140 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[141 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func OldChannels_InactiveWeek(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[142 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortHours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[143 * 6 + Int(form.rawValue)]!, stringValue) + } + public func InviteText_ContactsCountText(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[144 * 6 + Int(form.rawValue)]!, stringValue) } - public func InstantPage_Views(_ value: Int32) -> String { + public func Stats_GroupTopPosterMessages(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[145 * 6 + Int(form.rawValue)]!, stringValue) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index afc4cc9f0a..a054f36ae8 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -44,7 +44,10 @@ public struct PresentationResourcesRootController { } public static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) + return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.rootController.navigationBar.accentTextColor) + }) +// return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) } public static func navigationCallIcon(_ theme: PresentationTheme) -> UIImage? { diff --git a/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift b/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift new file mode 100644 index 0000000000..ecc1edd112 --- /dev/null +++ b/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift @@ -0,0 +1,35 @@ +import Foundation +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import SyncCore + +public func stringForFullAuthorName(message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> String { + var authorString = "" + if let author = message.author, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace) { + var authorName = "" + if author.id == accountPeerId { + authorName = strings.DialogList_You + } else { + authorName = author.compactDisplayTitle + } + if let peer = message.peers[message.id.peerId], author.id != peer.id { + authorString = "\(authorName) → \(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder))" + } else { + authorString = authorName + } + } else if let peer = message.peers[message.id.peerId] { + if message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + authorString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } else { + if message.id.peerId == accountPeerId { + authorString = strings.DialogList_SavedMessages + } else if message.flags.contains(.Incoming) { + authorString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } else { + authorString = "\(strings.DialogList_You) → \(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder))" + } + } + } + return authorString +} diff --git a/submodules/TelegramUI/Resources/PresentationStrings.mapping b/submodules/TelegramUI/Resources/PresentationStrings.mapping index 2948d62a31f23ecaa845beb1bda333b783739f73..c4e990eba2bbd5c045c82e6f74cec9276781c13b 100644 GIT binary patch delta 4774 zcmZu!3v`p$k@lTbej8gzHnQc1{K6Qp{C;6$TiEg=Se9(bHZ~Yr*gv*FmW+OxK$-;D zg>Fa*f&8tfB`u-5n`GOx1^PaDQT8+^?Y4Q)-8Sv!G=xp|l{6$|ce~r&Y_i||C1}Z6 z&N0%RJ9qBfnQy)sJ@kCa_nuD?ZyyoIg!rzYb~|5NPkE``3;w8@fPcT_O?Ij%eKji~ zAqPw6Tp;ROP!<(CG(z)AcQ_c1=!(DMqjoBzUuS9OpD5rL>|_$J@oRRniQn*tb}AIF zv$dMmiL2aGO(o(Do|mg@e5#t7#XshHQp^g*UC-*(-=;r6%@UP!{A~Uy$Z#Gha1xTRr^vJv-~E)bihurom2kcyTctvJWW- zqVo3t;luS*Ek5K6^|V&}FTW~3KH`t+$ss=Gt_HZ0VB~8*7Z&Z6$5Z)u17(shYL{F& ze4&9Xg7_y5R4VS^cN-ALPjO}=Sq%nbLPGhA*IeH|w4aBGSdb{bL!Lc7EA zcsh4A0RkC3-9#mZRr0uY{TCKK+eA5rOe_nv;w3Y`*hEH=#aEkXgD`ShGdx|*Rn1gZ zYJyHS42}DPK_zM*U7GVpl{vd3+JjFS_G}{?mxYO6E}}$!u$huY4zD!R2E!Vhv}<2E zVCAb8kNt;krZkuP5do@Vx>@ZYr}OJ)3SE86@sKDvVm~8B4NXx3|uy zU3{lde?+RD7kNn~AKpY|Vm&{$32E7YTO8WHw<`IiMlx$lZD|HOL?zmhMGM>85Zh|* zYQyttjNCOzYj}Sft)^O|_DFd_YIjhHT~x;x+i0Vx=hxd{x`FR#$EuNQ+NnY`^R{-t zpoQ;iM{ZjATszekY{DG?ZGtNtiZ0l_3*kuA<&VrRNC(^adOKM~JEv|2-Zn$is_lEq z%pIGNhEAg_MJ;q9JyGg*8QJ!MDAcNdoy2E0lZhNg&MH9&f4Z4cXiGd+=ZEqW>!f-& zr*#0)JzU*ERl>^pKbU;76K00^*-pg4&2M&s#D+P&3r0tH zV;A1)<^C@C<>O!%t))>T3m*YVla!bufjS0r#aieQOSa1&^3MlC^ZiP2Nj_*CJ61kY zFY=RW>!j*7ey@*m*!HS07^K1kZ*YJ*Cwa(0rH1WNT^rtsvfxGkOWG>l4Cc_M5Hop3vuw?0+xeyM{#bE5` zU0aa&UHrfn5Y8-L+yYB;{Q4H;e_^E?K@8yR+J_hOdFNXqiHEu=RqWx3ZmJi1`N3{# zp`cMaeY}uQZ4=24?@6o#O8m1?bwp7Wo!J&~J{^vC`xljnKgK!owIR;$K^DT?)PtLs zc&Z209D#B<*H$5-kM)pQsQgq9T1=E*>p^6e`J*0C`F^f(Ql&V+lTNBwa}Y;Tr2ROd z>V$MRswvgP_Xb58f6qx~`m~WhniXp^2K{Q(t0*Dq!9D!36V-Sh7xv<{hq$Ykn&^lm z)k0qBr7Xiym^Ny~)n+|C$M{FRXilGjFrUvfqadY``#GZzG;^Fc^wE0p0FU)inc=h0 zvGE%}6WRQ^KGfg|BYPUqG@j|B+`I>|nSqnt;5roz;4b+^nmQ%%P$W)sW^ErQNeiFr zr&8PJv0RN~o>6cAba%hQH$CF?jgAaC^}fbN?TRUvKkP>>e!H5g5|LoF z`}|A3@XhC*<9&D1M*4H3Y^Wy9mTpLP!~*dbeCbYFXZTATFVY@*yr?YRBp-u7XCn0e z?uBqzRUA5;WiTG+>aF03CwOWrj{X%it=f%cGoRfGCi<#TGafUUyg@`rIuM#w^oW1W z$PNW}pPWmiCyhK)DsnX2Swn{J;F2PZeqDONm;WBkrfLwK{Ttjdhzxy`cMhU5F6jQ{ zf76=Y9S-ddMD(Ow#-F0Zn~4h<^2LHn`CDzQeWh?_n{y9EAgx6id#o2srqp(QeC;Sl0 zQrQB{F-YBNAAgOFD$FI>>@J}!-r%Op>>ojvj6-sM>JKmK&wQTUZps%gaL^5A`!Sz# zlU@9TFS(JPpBnklWpvR*4`tDdsD|?vuJNG9z07?cDm1(TdwFd89ngKBhcd(!KH)*d z{+z$zp(>sprtBigb#Wv|2Lo!a@b`aL5@Xf-W7e!8) zt|d!Nn&=Hj?(#?GlsU(86pcuAM;DZcn#h?Wa6E~pMp4hn{ChtUiT(M>a|La|*$^sR6jXuK0y&4RJ_U1wsBU)VT3WS!U zikineXVA#3d};?(9?FNtZBf*p8PA4VfL)n^_}Ui$U)%MjuNFcqy&eed4@CXZ00vi= zA}1lW2+}q9`^4bZkMUA<%!h5*lK#ry8-x7F&p^lT`%w(V9P*?0mSAf=*ACNKc5SC5 zQOZxr43=@>6y>Ll4fHtO-g4*^^5rqgO>rKGVpvz^*7CDHs8#U&<5W_$4hk|J!^?r$ zy>gnDqZ)u!3E8c9#IvxT9aBKc20k-NMI{@dA*1FF&c#_)wGV{Qy#o6c{oz$I>Pen{ zx6kdh^YvlCs+!O5#N9PqHip2~^6XB0)ah8`PA?g<CO;cMVWtYdrLu zU`b~~Tohw{Rllqa^4lR82SYno4WqC(<55NR9dTd0$ftURI9HN;DO2T{ybStVSLGwS2esnRN z_l!|G#=qBRkoXb4Hi5)@`8V6iTJ3{58KGN*1UJ-C2xR1M3DFnyVQ7p`4&&^&%xzli zUE83S0}{wK^p5vi{q4M zI0mtar(X~`&7Z+n$vuDkFfiub`n&!N8}ePZQNOI3`}rlwZ^!v%;CI6VP`T|z9)w5z vETlT&#ZaY3_ym7>8~WOV{LnNWds6aPk$6bbc5b4saEhH?y#KTWaq0g79D@F^ delta 4944 zcmZWt3wTr4k=B{y`XSp`7s;~Zhb+G^5Oe(kV{C&h+cGv-wrt4{p7ynMu@z)pp(_)d zZU}BeTk_zMyPs#9CM2}mW>cC%`pRw-Hu&3Z)30A1WV3D3ynt=fHcJ=MZg)3nn`Gx) z36^xf4?b~b=FFLM=AVD&^8FuX9{XV?dvAdqX6$wJ}1ZL zx%zEV7o%&MxzZzs;2qps17++Y9<70u!X;8y>+G~cf4npYFV(;@b{VhLK&9|+q$$#$ zYb?wRB-9vp^$Xls3(dm2q^Lr96|?G=o(z5B*Jkr@G!c$4GWivruB9vd8egmhH~S4< zt%WY(J=(Au{r9p$9I695`*#f2K?z*3=(z{-@XSLOaN%dnW}yrC)kuUSrau_L{J9B=@n7q~Cj1wz%kcxrkmx|LAJmT_!GX?04vy^@niZC zJ|c@oyxhVn(B)wn`uQV*uKs7X@G;2?^w^_LbB_{<@qM3I@bbSfhkoJfW<0wVa+wLw zuZ0@M@cp%PBA{&@l$PEinoP9Sv^lQM#U#(9I+u{Hd$^T!m5Z;lJpI`X8Tei+V-_QEzl3WpN~=_UEWN~GJW4BEBYHL zX<0bk2reO;r1|=_FWK?2M#vCyXj!FS{iy?Ax`k!u4k`Q9y#^|mk;0{y#%%b9Mi7Nu zTDtLDZ?U5KfHIW`t5F_?g`{@+?Nn!Ds-{StGjcSlM10EZq!QP-g-8}oTsX(_QC$z2 zY&p)a2RGP66z7-~UoL^P%mKZLcHi;*%nVI6Qmb9XxB0_2qfeL)z^L{zCWzo!M8Y3ZK6jiz+;3NC={{O z%^c>n99~7V$&IJdAP?Vdg5|6jKWG9Eb7Auaa6yTP7gs}(xtj{rxQrVQZGiQx6#sq$ zR6?1EOIHcR6SrsLpEiJ_y_~%9Lhe(i!%@jQD~BVUYDno&L-YKwij;a(QTB%9`DCS2 z(bK<4Yg!@dbq_f*+;T8~?gwrihEYy&+L_bw#!$(@^jXjR;Y%xB5pav3UUn4N&9PgSdDmhE5U9(eyx>Aq6wdGg*vtYKWv4< z;%4&1f$dW?O`cYyK6OgIj*}Lw+X#iM6@43F1#A*IZ|Kqcv(vcKHWA&wXD+?w=jph# z5v({cyqk}{O$6XhT9%>vm(01QQ;qHk$7gSx z+C{2t+U}Rb(S+0;muHm_cjqN}rC!&WhyJIT8K2t(InYhGddz{p+C=ZR89&%WfayU; z8}UFdwzWZh;TGD&@ntYuXOAa1-skN$SZyob-$s1ghiBSgt>7cO0-l4#c7l69n%iOJ zMn4H{v^j~4fD#RHV2p(2z^nnRLDKWL8;*tKgd!z>Jh2#wOhc%)6MY0wZ>P5lig;)Y z99bj1UEYBJht)8?(@u#TAs2gu_CRMf9MkcaNqm|)P_7!G+T%xyU5fpTFx zH`fO@+H>gD67p2Sh+_wy_do&LiEnzKn(e}j4tnt2WSVLhXKWeR+W}TKj=MVGHa3Ar zItWWLKGi|7O^Ukb2Uh%T2RMW&+GN*{1)Rp&(^%C>PdS7AodlRL9_oY!xLw4fuTrhO z+6hkBE9xhXxbXNG%XskiG%V->OGAWw@ceaB;^K0W{b*^LB{@%pPmW1l;dqjcjQFFt ztBcN2@t!VR0($^HhjtE8Z0${(RdDHn>y7kEhrQbaq2F`wXt=1{b>vz1>g>2Suz)W3ELv zWaR{v1BpQRPJ>Dh@jj5xdo7;prt?0BFLgr^y9cjygIhRE#}?tzdMK&l2Y!K;yo2&_ zK}y~{sH%}f*Z{*9Nn3&kYFV+_7!>%9BY4+ls1WWYl^ZX;!1C~!&9EHq6Vbnx+3=6$ z%*pP@k2Zq?jwLZs-9ykkj*UG~&rV>thidsjysw8iMJFd~`80uVJwcA%>LI%MYrN7! z#JMQyuUMT~z0ug5!Ne%yzG-H)OPvu})523hHJW6}zabw^WMg2**Tj;e;;+vMwj~UrLO>RnX=?xNBcr5IjsPqweJc)<<2#II#n|&0+S$vIt;3;yo$n0o| zGc(ccfdmiZ+gRUl4d_SyW=kRPljfnq9uJiD*YUbeZHa$nna-+XG z2Ow8iqU9Qt`yn5@2B5h9pJ<&yX1uz3)LG+>|4cIaW8$59NYUmZ)M9T$_#@N@h*6#= z19D4k=H&sJtbR<(QoOhmoT7h*pwU0aX99lyPei%0CINA|N9x{Y^EVV3^oK zGKOjG7W9vS#BRlp{bVm-RHmOaJhg+md%DTU@#r?lp#kbinNH5chen7g%=qjelsov? zl}mrS)%YE9Jfwt1!XZV~vhhR^oGb?ywu7^Uh9TMwjC&V+0l}x`)7))zXNRP#>gHhtFUbtT&~rr>)m{NBTcuF!ikqhAwS=MLN%>9`iC_oZbZKZ-x#4GPBQ88GsGfK zWutiqF?tPN+f7}f7DMy{btZk`v=ytjgE=ic8;dCQ`X^5NGM-+ZU9EvU7GZ3HrrU2# zLIzulC1a2e>r6UZEXL>!E z6tfom@ouW|R&3u+TQ*|d1h^+QkvRvFF@*TMfq-YTH_%1`-Z7GsZwi4UH2iAqB<0D! zf#8%<^N^53_J+b3aXU=V}Q-=rjc3+c@kt8OK1ala1aX8jC}h!f}JTi5Axb{7Yxz9PhCHd2v6;$ z50d~|=FE`gU9zf}-LbB;+ID%@j)Yc!y7oD?5$g-Tt0|;6Pv(8S$w5WGxX; zlqiuf5rJn~-Hr33P~;pZ9Y6dg*chizVDk{Hq*+V~!YW~sEF6n7%)VtS#i&iuk|*3x ztvW{k!5{^uR49Hb9KBjKnLTO6e?ihZ~+ zO#fxU`$ImDIiEsL?QZ8Q*Wh<1bKJt`o=u_{ku+T}NpX56~D^ o4dYZlJjT80gVT8e=ZxccdK@a9CsNkEQ7s`y6Mp`cXz 0.0 { let currentFrame = selectionFrames[currentIndex] let previousFrame = selectionFrames[currentIndex - 1] @@ -425,7 +425,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat private let coveringBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let tabsContainerNode: PeerInfoPaneTabsContainerNode - private let tapsSeparatorNode: ASDisplayNode + private let tabsSeparatorNode: ASDisplayNode let isReady = Promise() var didSetIsReady = false @@ -471,15 +471,15 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.tabsContainerNode = PeerInfoPaneTabsContainerNode() - self.tapsSeparatorNode = ASDisplayNode() - self.tapsSeparatorNode.isLayerBacked = true + self.tabsSeparatorNode = ASDisplayNode() + self.tabsSeparatorNode.isLayerBacked = true super.init() self.addSubnode(self.separatorNode) self.addSubnode(self.coveringBackgroundNode) self.addSubnode(self.tabsContainerNode) - self.addSubnode(self.tapsSeparatorNode) + self.addSubnode(self.tabsSeparatorNode) self.tabsContainerNode.requestSelectPane = { [weak self] key in guard let strongSelf = self else { @@ -488,7 +488,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if strongSelf.currentPaneKey == key { if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { } else { - strongSelf.currentPane?.node.scrollToTop() + let _ = strongSelf.currentPane?.node.scrollToTop() } return } @@ -563,7 +563,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat cancelContextGestures(view: self.view) case .changed: - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / size.width if currentIndex <= 0 { @@ -576,7 +576,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) } case .cancelled, .ended: - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? @@ -648,7 +648,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { var nextCandidatePaneKey: PeerInfoPaneKey? - if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 { + if let index = previousAvailablePanes.firstIndex(of: currentPaneKey), index != 0 { for i in (0 ... index - 1).reversed() { if availablePanes.contains(previousAvailablePanes[i]) { nextCandidatePaneKey = previousAvailablePanes[i] @@ -671,7 +671,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat let currentIndex: Int? if let currentPaneKey = self.currentPaneKey { - currentIndex = availablePanes.index(of: currentPaneKey) + currentIndex = availablePanes.firstIndex(of: currentPaneKey) } else { currentIndex = nil } @@ -683,14 +683,14 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - self.tapsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let tabsHeight: CGFloat = 48.0 transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) - transition.updateFrame(node: self.tapsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) @@ -706,7 +706,6 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat } for index in visiblePaneIndices { - let indexOffset = CGFloat(index - currentIndex) let key = availablePanes[index] if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { requiredPendingKeys.append(key) @@ -781,8 +780,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.pendingSwitchToPaneKey = nil previousPaneKey = self.currentPaneKey self.currentPaneKey = pendingSwitchToPaneKey - updatedCurrentIndex = availablePanes.index(of: pendingSwitchToPaneKey) - if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.index(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { + updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) + if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { if updatedCurrentIndex < previousIndex { paneSwitchAnimationOffset = -size.width } else { @@ -794,7 +793,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat } for (key, pane) in self.currentPanes { - if let index = availablePanes.index(of: key), let updatedCurrentIndex = updatedCurrentIndex { + if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { var paneWasAdded = false if pane.node.supernode == nil { self.addSubnode(pane.node) @@ -811,7 +810,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat } pane.isAnimatingOut = false if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey), let paneIndex = availablePanes.index(of: key), abs(paneIndex - currentIndex) <= 1 { + if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { } else { if let pane = strongSelf.currentPanes.removeValue(forKey: key) { //print("remove \(key)")