From 80a7324668cc83111e114103810cbcfe0c44a953 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 22 Nov 2022 19:50:50 +0400 Subject: [PATCH] Forum improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 7 + .../Sources/ChatListSearchItemHeader.swift | 19 +- .../ChatListUI/Sources/ChatContextMenus.swift | 33 +- .../Sources/ChatListController.swift | 689 ++++++++++-------- .../Sources/ChatListControllerNode.swift | 99 +-- .../Sources/ChatListSearchContainerNode.swift | 5 + .../Sources/ChatListSearchListPaneNode.swift | 62 +- .../Sources/ChatListSelection.swift | 49 +- .../Sources/Node/ChatListItem.swift | 23 +- .../Sources/Node/ChatListNode.swift | 22 +- submodules/Display/Source/NavigationBar.swift | 4 +- submodules/SettingsUI/BUILD | 1 + .../PrivacyAndSecurityController.swift | 167 ++++- submodules/TelegramApi/Sources/Api0.swift | 3 + submodules/TelegramApi/Sources/Api29.swift | 45 +- submodules/TelegramApi/Sources/Api4.swift | 36 + .../Sources/Settings/PrivacySettings.swift | 7 +- .../Peers/ChannelCreation.swift | 2 +- .../TelegramEngine/Peers/CreateGroup.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 13 + .../Privacy/TelegramEnginePrivacy.swift | 4 + .../UpdatedAccountPrivacySettings.swift | 28 +- .../Sources/ChatListHeaderComponent.swift | 2 + .../Sources/ChatTimerScreen.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 8 +- .../Sources/ChatHistoryListNode.swift | 10 +- .../Sources/NavigateToChatController.swift | 26 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 119 ++- .../Sources/PeerInfo/PeerInfoScreen.swift | 11 +- .../UrlHandling/Sources/UrlHandling.swift | 19 +- 30 files changed, 1076 insertions(+), 443 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5800948e8b..6fbb8b356e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8302,3 +8302,10 @@ Sorry for the inconvenience."; "CreateTopic.ShowGeneral" = "Show in Topics"; "CreateTopic.ShowGeneralInfo" = "If the 'General' topic is hidden, group members can pull down in the topic list to view it."; + +"ChatList.DeleteThreadsConfirmation_1" = "Delete"; +"ChatList.DeleteThreadsConfirmation_any" = "Delete %@ Threads"; +"ChatList.DeletedThreads_1" = "Deleted 1 thread"; +"ChatList.DeletedThreads_any" = "Deleted %@ threads"; + +"DialogList.SearchSectionMessagesIn" = "Messages in %@"; diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 060264c842..4561e8d691 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -21,7 +21,7 @@ public enum ChatListSearchItemHeaderType { case chats case chatTypes case faq - case messages + case messages(location: String?) case groupMembers case activeVoiceChats case recentCalls @@ -65,8 +65,12 @@ public enum ChatListSearchItemHeaderType { return strings.ChatList_ChatTypesSection case .faq: return strings.Settings_FrequentlyAskedQuestions - case .messages: - return strings.DialogList_SearchSectionMessages + case let .messages(location): + if let location { + return strings.DialogList_SearchSectionMessagesIn(location).string + } else { + return strings.DialogList_SearchSectionMessages + } case .groupMembers: return strings.Group_GroupMembersHeader case .activeVoiceChats: @@ -120,8 +124,12 @@ public enum ChatListSearchItemHeaderType { return .chatTypes case .faq: return .faq - case .messages: - return .messages + case let .messages(location): + if let _ = location { + return .messagesWithLocation + } else { + return .messages + } case .groupMembers: return .groupMembers case .activeVoiceChats: @@ -160,6 +168,7 @@ private enum ChatListSearchItemHeaderId: Int32 { case chatTypes case faq case messages + case messagesWithLocation case photos case links case files diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 9cb7a38ad3..95462eb246 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -172,6 +172,11 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch if readCounters.isUnread { isUnread = true } + + var isForum = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + isForum = true + } if case let .chatList(currentFilter) = source { if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { @@ -317,7 +322,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).start() f(.default) }))) - } else { + } else if !isForum { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).start() f(.default) @@ -494,15 +499,12 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let strings = presentationData.strings - return combineLatest( - context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ), - context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: threadId) - ) + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), + TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: threadId) ) - |> mapToSignal { peer, threadData -> Signal<[ContextMenuItem], NoError> in + |> mapToSignal { peer, peerNotificationSettings, threadData -> Signal<[ContextMenuItem], NoError> in guard case let .channel(channel) = peer else { return .single([]) } @@ -548,12 +550,19 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } var isMuted = false - if case .muted = threadData.notificationSettings.muteState { + switch threadData.notificationSettings.muteState { + case .muted: isMuted = true + case .unmuted: + isMuted = false + case .default: + if case .muted = peerNotificationSettings.muteState { + isMuted = true + } } items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] c, f in if isMuted { - let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: threadId) + let _ = (context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: 0) |> deliverOnMainQueue).start(completed: { f(.default) }) @@ -776,7 +785,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) { - let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, peerId: peerId, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in + let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in let presentationData = context.sharedContext.currentPresentationData.with { $0 } if value <= 0 { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 510b89793b..d78b5e341e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -142,6 +142,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var pendingSecondaryContext: ChatListLocationContext? private var secondaryContext: ChatListLocationContext? + fileprivate var effectiveContext: ChatListLocationContext? { + return self.secondaryContext ?? self.primaryContext + } + + public var effectiveLocation: ChatListControllerLocation { + return self.secondaryContext?.location ?? self.location + } + private var badgeDisposable: Disposable? private var badgeIconDisposable: Disposable? @@ -195,7 +203,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { - self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) + self.chatListDisplayNode.effectiveContainerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) } } @@ -277,7 +285,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController location: self.location, parentController: self, hideNetworkActivityStatus: self.hideNetworkActivityStatus, - chatListDisplayNode: self.chatListDisplayNode, + containerNode: self.chatListDisplayNode.mainContainerNode, isReorderingTabs: self.isReorderingTabsValue.get() ) self.primaryContext = primaryContext @@ -357,19 +365,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if strongSelf.chatListDisplayNode.searchDisplayController != nil { strongSelf.deactivateSearch(animated: true) } else { - switch strongSelf.chatListDisplayNode.containerNode.currentItemNode.visibleContentOffset() { + switch strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.visibleContentOffset() { case .none, .unknown: if let searchContentNode = strongSelf.searchContentNode { searchContentNode.updateExpansionProgress(1.0, animated: true) } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top) case let .known(offset): - let isFirstFilter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.containerNode.availableFilters.first?.filter + let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.mainContainerNode.availableFilters.first?.filter if offset <= navigationBarSearchContentHeight + 1.0 && strongSelf.chatListDisplayNode.inlineStackContainerNode != nil { strongSelf.setInlineChatList(location: nil) } else if offset <= navigationBarSearchContentHeight + 1.0 && !isFirstFilter { - let firstFilter = strongSelf.chatListDisplayNode.containerNode.availableFilters.first ?? .all + let firstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.availableFilters.first ?? .all let targetTab: ChatListFilterTabEntryId switch firstFilter { case .all: @@ -385,7 +393,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let inlineStackContainerNode = strongSelf.chatListDisplayNode.inlineStackContainerNode { inlineStackContainerNode.currentItemNode.scrollToPosition(.top) } else { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top) } } } @@ -711,7 +719,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if case .chatList(.root) = self.location { - self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in + self.chatListDisplayNode.mainContainerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in guard let strongSelf = self else { return } @@ -724,7 +732,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if force { strongSelf.tabContainerNode.cancelAnimations() } - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: tabContainerData.2, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: tabContainerData.2, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) } self.reloadFilters() } @@ -856,7 +864,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) if let layout = self.validLayout { - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.effectiveContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) } if self.isNodeLoaded { @@ -875,74 +883,74 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self?.deactivateSearch(animated: true) } - self.chatListDisplayNode.containerNode.activateSearch = { [weak self] in + self.chatListDisplayNode.mainContainerNode.activateSearch = { [weak self] in self?.activateSearch() } - self.chatListDisplayNode.containerNode.presentAlert = { [weak self] text in + self.chatListDisplayNode.mainContainerNode.presentAlert = { [weak self] text in if let strongSelf = self { self?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } - self.chatListDisplayNode.containerNode.present = { [weak self] c in + self.chatListDisplayNode.mainContainerNode.present = { [weak self] c in if let strongSelf = self { strongSelf.present(c, in: .window(.root)) } } - self.chatListDisplayNode.containerNode.push = { [weak self] c in + self.chatListDisplayNode.mainContainerNode.push = { [weak self] c in if let strongSelf = self { strongSelf.push(c) } } - self.chatListDisplayNode.containerNode.toggleArchivedFolderHiddenByDefault = { [weak self] in + self.chatListDisplayNode.mainContainerNode.toggleArchivedFolderHiddenByDefault = { [weak self] in guard let strongSelf = self else { return } strongSelf.toggleArchivedFolderHiddenByDefault() } - self.chatListDisplayNode.containerNode.hidePsa = { [weak self] peerId in + self.chatListDisplayNode.mainContainerNode.hidePsa = { [weak self] peerId in guard let strongSelf = self else { return } strongSelf.hidePsa(peerId) } - self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId, joined in + self.chatListDisplayNode.mainContainerNode.deletePeerChat = { [weak self] peerId, joined in guard let strongSelf = self else { return } strongSelf.deletePeerChat(peerId: peerId, joined: joined) } - self.chatListDisplayNode.containerNode.deletePeerThread = { [weak self] peerId, threadId in + self.chatListDisplayNode.mainContainerNode.deletePeerThread = { [weak self] peerId, threadId in guard let strongSelf = self else { return } strongSelf.deletePeerThread(peerId: peerId, threadId: threadId) } - self.chatListDisplayNode.containerNode.setPeerThreadStopped = { [weak self] peerId, threadId, isStopped in + self.chatListDisplayNode.mainContainerNode.setPeerThreadStopped = { [weak self] peerId, threadId, isStopped in guard let strongSelf = self else { return } strongSelf.setPeerThreadStopped(peerId: peerId, threadId: threadId, isStopped: isStopped) } - self.chatListDisplayNode.containerNode.setPeerThreadPinned = { [weak self] peerId, threadId, isPinned in + self.chatListDisplayNode.mainContainerNode.setPeerThreadPinned = { [weak self] peerId, threadId, isPinned in guard let strongSelf = self else { return } strongSelf.setPeerThreadPinned(peerId: peerId, threadId: threadId, isPinned: isPinned) } - self.chatListDisplayNode.containerNode.setPeerThreadHidden = { [weak self] peerId, threadId, isHidden in + self.chatListDisplayNode.mainContainerNode.setPeerThreadHidden = { [weak self] peerId, threadId, isHidden in guard let strongSelf = self else { return } strongSelf.setPeerThreadHidden(peerId: peerId, threadId: threadId, isHidden: isHidden) } - self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, threadId, animated, activateInput, promoInfo in + self.chatListDisplayNode.mainContainerNode.peerSelected = { [weak self] peer, threadId, animated, activateInput, promoInfo in if let strongSelf = self { if let navigationController = strongSelf.navigationController as? NavigationController { var scrollToEndIfExists = false @@ -975,8 +983,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let chatLocation: NavigateToChatControllerParams.Location chatLocation = .peer(peer) - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: chatLocation, activateInput: (activateInput && !peer.isDeleted) ? .text : nil, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: navigationAnimationOptions, parentGroupId: groupId._asGroup(), chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in - self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: chatLocation, activateInput: (activateInput && !peer.isDeleted) ? .text : nil, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: navigationAnimationOptions, parentGroupId: groupId._asGroup(), chatListFilter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in + self?.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) if let promoInfo = promoInfo { switch promoInfo { case .proxy: @@ -1018,30 +1026,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - self.chatListDisplayNode.containerNode.groupSelected = { [weak self] groupId in + self.chatListDisplayNode.mainContainerNode.groupSelected = { [weak self] groupId in if let strongSelf = self { if let navigationController = strongSelf.navigationController as? NavigationController { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupId), controlsHistoryPreload: false, enableDebugActions: false) chatListController.navigationPresentation = .master navigationController.pushViewController(chatListController) - strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) } } } - self.chatListDisplayNode.containerNode.updatePeerGrouping = { [weak self] peerId, group in + self.chatListDisplayNode.mainContainerNode.updatePeerGrouping = { [weak self] peerId, group in guard let strongSelf = self else { return } if group { strongSelf.archiveChats(peerIds: [peerId]) } else { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) let _ = strongSelf.context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: group ? .archive : .root).start(completed: { guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(nil) }) } } @@ -1069,7 +1077,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }, scrollToEndIfExists: scrollToEndIfExists, options: navigationAnimationOptions)) } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) } } })) @@ -1100,7 +1108,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self?.deactivateSearch(animated: false) }, scrollToEndIfExists: scrollToEndIfExists, options: navigationAnimationOptions)) } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) } } })) @@ -1177,10 +1185,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else { return } - if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter { + if let filter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter { strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in })) } else { - if case let .forum(peerId) = strongSelf.location { + if case let .forum(peerId) = strongSelf.chatListDisplayNode.effectiveContainerNode.location { let context = strongSelf.context let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) controller.navigationPresentation = .modal @@ -1213,7 +1221,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self?.toolbarActionSelected(action: action) } - self.chatListDisplayNode.containerNode.activateChatPreview = { [weak self] item, threadId, node, gesture, location in + self.chatListDisplayNode.mainContainerNode.activateChatPreview = { [weak self] item, threadId, node, gesture, location in guard let strongSelf = self else { gesture?.cancel() return @@ -1246,12 +1254,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: strongSelf.chatListDisplayNode.inlineStackContainerNode == nil) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } else { @@ -1264,7 +1272,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } case let .forum(pinnedIndex, _, threadId, _, _): @@ -1282,7 +1290,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: strongSelf.chatListDisplayNode.inlineStackContainerNode == nil) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -1311,135 +1319,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.presentInGlobalOverlay(contextController) } - let context = self.context - let location = self.location - let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set, Set)?, NoError> = self.chatListDisplayNode.containerNode.currentItemState - |> map { state, filterId -> (Set, Set, Int32?)? in - if !state.editing { - return nil - } - return (state.selectedPeerIds, state.selectedThreadIds, filterId) - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - if lhs?.0 != rhs?.0 { - return false - } - if lhs?.1 != rhs?.1 { - return false - } - if lhs?.2 != rhs?.2 { - return false - } - return true - }) - |> mapToSignal { selectedPeerIdsAndFilterId -> Signal<(ChatListSelectionOptions, Set, Set)?, NoError> in - if let (selectedPeerIds, selectedThreadIds, filterId) = selectedPeerIdsAndFilterId { - switch location { - case .chatList: - return chatListSelectionOptions(context: context, peerIds: selectedPeerIds, filterId: filterId) - |> map { options -> (ChatListSelectionOptions, Set, Set)? in - return (options, selectedPeerIds, selectedThreadIds) - } - case let .forum(peerId): - return forumSelectionOptions(context: context, peerId: peerId, threadIds: selectedThreadIds, canDelete: false) - |> map { options -> (ChatListSelectionOptions, Set, Set)? in - return (options, selectedPeerIds, selectedThreadIds) - } - } - - } else { - return .single(nil) - } - } - - let peerView: Signal - if case let .forum(peerId) = location { - peerView = context.account.viewTracker.peerView(peerId) - |> map(Optional.init) - } else { - peerView = .single(nil) - } - - let previousToolbarValue = Atomic(value: nil) - self.stateDisposable.set(combineLatest(queue: .mainQueue(), - self.presentationDataValue.get(), - peerIdsAndOptions, - peerView - ).start(next: { [weak self] presentationData, peerIdsAndOptions, peerView in - guard let strongSelf = self else { - return - } - var toolbar: Toolbar? - if let (options, peerIds, _) = peerIdsAndOptions { - if case .chatList(.root) = strongSelf.location { - let leftAction: ToolbarAction - switch options.read { - case let .all(enabled): - leftAction = ToolbarAction(title: presentationData.strings.ChatList_ReadAll, isEnabled: enabled) - case let .selective(enabled): - leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) - } - var archiveEnabled = options.delete - var displayArchive = true - if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { - if !data.excludeArchived { - displayArchive = false - } - } - if archiveEnabled { - for peerId in peerIds { - if peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) { - archiveEnabled = false - break - } else if peerId == strongSelf.context.account.peerId { - archiveEnabled = false - break - } - } - } - toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: displayArchive ? ToolbarAction(title: presentationData.strings.ChatList_ArchiveAction, isEnabled: archiveEnabled) : nil) - } else if case .forum = strongSelf.location { - let leftAction: ToolbarAction - switch options.read { - case .all: - leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false) - case let .selective(enabled): - leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) - } - toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: nil) - } else { - let middleAction = ToolbarAction(title: presentationData.strings.ChatList_UnarchiveAction, isEnabled: !peerIds.isEmpty) - let leftAction: ToolbarAction - switch options.read { - case .all: - leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false) - case let .selective(enabled): - leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) - } - toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: middleAction) - } - } else if let peerView = peerView, let channel = peerView.peers[peerView.peerId] as? TelegramChannel { - switch channel.participationStatus { - case .member: - toolbar = nil - default: - let actionTitle: String - if channel.flags.contains(.requestToJoin) { - actionTitle = strongSelf.presentationData.strings.Group_ApplyToJoin - } else { - actionTitle = strongSelf.presentationData.strings.Channel_JoinChannel - } - toolbar = Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: actionTitle, isEnabled: true)) - } - } - var transition: ContainedViewLayoutTransition = .immediate - let previousToolbar = previousToolbarValue.swap(toolbar) - if (previousToolbar == nil) != (toolbar == nil) { - transition = .animated(duration: 0.4, curve: .spring) - } - strongSelf.setToolbar(toolbar, transition: transition) - })) - self.tabContainerNode.tabSelected = { [weak self] id, isDisabled in guard let strongSelf = self else { return @@ -1491,7 +1370,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = combineLatest( queue: Queue.mainQueue(), strongSelf.context.engine.peers.currentChatListFilters(), - context.engine.data.get( + strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ) ).start(next: { [weak self] filters, premiumLimits in @@ -1564,8 +1443,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { let _ = combineLatest( queue: Queue.mainQueue(), - context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + strongSelf.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ), @@ -1584,14 +1463,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let premiumLimit = premiumLimits.maxFolderChatsCount if data.includePeers.peers.count >= premiumLimit { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {}) + let controller = PremiumLimitScreen(context: strongSelf.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {}) strongSelf.push(controller) f(.dismissWithoutContent) return } else if data.includePeers.peers.count >= limit && !isPremium { var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + let controller = PremiumLimitScreen(context: strongSelf.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: { + let controller = PremiumIntroScreen(context: strongSelf.context, source: .chatsPerFolder) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in @@ -1679,7 +1558,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.ready.set(.never()) } else { self.ready.set(combineLatest([ - self.chatListDisplayNode.containerNode.ready, + self.chatListDisplayNode.mainContainerNode.ready, self.primaryInfoReady.get() ]) |> map { values -> Bool in @@ -1704,15 +1583,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.didAppear = true - self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(true) + self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true) - self.chatListDisplayNode.containerNode.didBeginSelectingChats = { [weak self] in + self.chatListDisplayNode.mainContainerNode.didBeginSelectingChats = { [weak self] in guard let strongSelf = self else { return } if !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing { var isEditing = false - strongSelf.chatListDisplayNode.containerNode.updateState { state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState { state in isEditing = state.editing return state } @@ -1726,7 +1605,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - self.chatListDisplayNode.containerNode.displayFilterLimit = { [weak self] in + self.chatListDisplayNode.mainContainerNode.displayFilterLimit = { [weak self] in guard let strongSelf = self else { return } @@ -1908,7 +1787,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) } - self.chatListDisplayNode.containerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in + self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in guard let strongSelf = self else { return } @@ -2018,7 +1897,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(false) + self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false) self.dismissAllUndoControllers() @@ -2033,7 +1912,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.deactivateSearch(animated: false) } - self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + self.chatListDisplayNode.clearHighlightAnimated(true) } func requestUpdateHeaderContent(transition: ContainedViewLayoutTransition) { @@ -2167,7 +2046,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.tabContainerNode.isUserInteractionEnabled = self.chatListDisplayNode.inlineStackContainerNode == nil transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0))) - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, visualNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition) } @@ -2177,22 +2056,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } @objc fileprivate func editPressed() { - if case .chatList(.root) = self.location { - self.primaryContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( - content: .text(title: self.presentationData.strings.Common_Done, isBold: true), - pressed: { [weak self] _ in - self?.donePressed() - } - ))) - (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.details, transition: .animated(duration: 0.5, curve: .spring)) - } else { - self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( - content: .text(title: self.presentationData.strings.Common_Done, isBold: true), - pressed: { [weak self] _ in - self?.donePressed() - } - ))) - (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.master, transition: .animated(duration: 0.5, curve: .spring)) + if self.secondaryContext == nil { + if case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location { + self.effectiveContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.donePressed() + } + ))) + (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.details, transition: .animated(duration: 0.5, curve: .spring)) + } else { + self.effectiveContext?.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.donePressed() + } + ))) + (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.master, transition: .animated(duration: 0.5, curve: .spring)) + } } self.requestUpdateHeaderContent(transition: .animated(duration: 0.3, curve: .spring)) @@ -2200,7 +2081,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.searchContentNode?.setIsEnabled(false, animated: true) self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false - self.chatListDisplayNode.containerNode.updateState { state in + self.chatListDisplayNode.effectiveContainerNode.updateState { state in var state = state state.editing = true state.peerIdWithRevealedOptions = nil @@ -2218,7 +2099,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring)) self.searchContentNode?.setIsEnabled(true, animated: true) self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false - self.chatListDisplayNode.containerNode.updateState { state in + self.chatListDisplayNode.effectiveContainerNode.updateState { state in var state = state state.editing = false state.peerIdWithRevealedOptions = nil @@ -2281,12 +2162,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController func setInlineChatList(location: ChatListControllerLocation?) { if let location { + let inlineNode = self.chatListDisplayNode.makeInlineChatList(location: location) let pendingSecondaryContext = ChatListLocationContext( context: self.context, location: location, parentController: self, hideNetworkActivityStatus: false, - chatListDisplayNode: self.chatListDisplayNode, + containerNode: inlineNode, isReorderingTabs: .single(false) ) self.pendingSecondaryContext = pendingSecondaryContext @@ -2297,12 +2179,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let self, let pendingSecondaryContext = pendingSecondaryContext, self.pendingSecondaryContext === pendingSecondaryContext else { return } + + if self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing { + self.donePressed() + } + self.secondaryContext = pendingSecondaryContext - self.chatListDisplayNode.setInlineChatList(location: location) + self.setToolbar(pendingSecondaryContext.toolbar, transition: .animated(duration: 0.5, curve: .spring)) + self.chatListDisplayNode.setInlineChatList(inlineStackContainerNode: inlineNode) }) } else { + if self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing { + self.donePressed() + } + self.secondaryContext = nil - self.chatListDisplayNode.setInlineChatList(location: nil) + self.setToolbar(self.primaryContext?.toolbar, transition: .animated(duration: 0.5, curve: .spring)) + self.chatListDisplayNode.setInlineChatList(inlineStackContainerNode: nil) } } @@ -2508,7 +2401,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController firstItemEntryId = .filter(id) } - var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.containerNode.currentItemFilter + var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.mainContainerNode.currentItemFilter var resetCurrentEntry = false if !resolvedItems.contains(where: { $0.id == selectedEntryId }) { resetCurrentEntry = true @@ -2550,19 +2443,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if !hasAllChats { availableFilters.insert(.all, at: 0) } - strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) + strongSelf.chatListDisplayNode.mainContainerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) if isPremium == nil && items.isEmpty { - strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + strongSelf.ready.set(strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.ready) } else if !strongSelf.initializedFilters { - if selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { - strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in + if selectedEntryId != strongSelf.chatListDisplayNode.mainContainerNode.currentItemFilter { + strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in if let strongSelf = self { - strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + strongSelf.ready.set(strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.ready) } }) } else { - strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + strongSelf.ready.set(strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.ready) } strongSelf.initializedFilters = true } @@ -2585,7 +2478,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.containerLayoutUpdated(layout, transition: transition) (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } else { - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.mainContainerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) } } @@ -2639,13 +2532,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController updatedFilter = nil } } - if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == updatedFilter?.id { + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == updatedFilter?.id { strongSelf.scrollToTop?() } else { if strongSelf.chatListDisplayNode.inlineStackContainerNode != nil { strongSelf.setInlineChatList(location: nil) } - strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: updatedFilter.flatMap { .filter($0.id) } ?? .all) + strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: updatedFilter.flatMap { .filter($0.id) } ?? .all) } }) } @@ -2668,8 +2561,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == id { - if strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing { + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing { strongSelf.donePressed() } } @@ -2679,8 +2572,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }).start() } - if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == id { - strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: .all, completion: { + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { + strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: .all, completion: { commit() }) } else { @@ -2703,6 +2596,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false) { + var filter = filter + if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { + filter = .topics + } + if self.displayNavigationBar { if !skipScrolling, let searchContentNode = self.searchContentNode, searchContentNode.expansionProgress != 1.0 { self.scrollToTop?() @@ -2712,7 +2610,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let _ = (combineLatest(self.chatListDisplayNode.containerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) + let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] _, chatListView in guard let strongSelf = self else { return @@ -2887,22 +2785,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let inputShortcuts: [KeyShortcut] = [ KeyShortcut(title: strings.KeyCommand_JumpToPreviousChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate], action: { [weak self] in if let strongSelf = self { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.previous(unread: false)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.previous(unread: false)) } }), KeyShortcut(title: strings.KeyCommand_JumpToNextChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate], action: { [weak self] in if let strongSelf = self { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.next(unread: false)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.next(unread: false)) } }), KeyShortcut(title: strings.KeyCommand_JumpToPreviousUnreadChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate, .shift], action: { [weak self] in if let strongSelf = self { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.previous(unread: true)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.previous(unread: true)) } }), KeyShortcut(title: strings.KeyCommand_JumpToNextUnreadChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate, .shift], action: { [weak self] in if let strongSelf = self { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.next(unread: true)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.next(unread: true)) } }), KeyShortcut(title: strings.KeyCommand_NewMessage, input: "N", modifiers: [.command], action: { [weak self] in @@ -2921,7 +2819,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let openTab: (Int) -> Void = { [weak self] index in if let strongSelf = self { - let filters = strongSelf.chatListDisplayNode.containerNode.availableFilters + let filters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters if index > filters.count - 1 { return } @@ -2937,9 +2835,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let openChat: (Int) -> Void = { [weak self] index in if let strongSelf = self { if index == 0 { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.peerId(strongSelf.context.account.peerId)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.peerId(strongSelf.context.account.peerId)) } else { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.index(index - 1)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.selectChat(.index(index - 1)) } } } @@ -2964,26 +2862,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } override public func toolbarActionSelected(action: ToolbarActionOption) { - let peerIds = self.chatListDisplayNode.containerNode.currentItemNode.currentState.selectedPeerIds - let threadIds = self.chatListDisplayNode.containerNode.currentItemNode.currentState.selectedThreadIds + let peerIds = self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.selectedPeerIds + let threadIds = self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.selectedThreadIds if case .left = action { let signal: Signal var completion: (() -> Void)? - if !threadIds.isEmpty, case let .forum(peerId) = self.location { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first!)) + if !threadIds.isEmpty, case let .forum(peerId) = self.chatListDisplayNode.effectiveContainerNode.location { + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first!)) completion = { [weak self] in - self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) } signal = self.context.engine.messages.markForumThreadsAsRead(peerId: peerId, threadIds: Array(threadIds)) } else if !peerIds.isEmpty { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) completion = { [weak self] in - self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) } signal = self.context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: Array(peerIds), setToValue: false) - } else if case let .chatList(groupId) = self.location { + } else if case let .chatList(groupId) = self.chatListDisplayNode.effectiveContainerNode.location { let filterPredicate: ChatListFilterPredicate? - if let filter = self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { + if let filter = self.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { filterPredicate = chatListFilterPredicate(filter: data) } else { filterPredicate = nil @@ -3005,8 +2903,91 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion?() }) } else if case .right = action { - if !threadIds.isEmpty { + if !threadIds.isEmpty, case let .forum(peerId) = self.chatListDisplayNode.effectiveContainerNode.location { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteThreadsConfirmation(Int32(threadIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for threadId in threadIds { + state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) + } + return state + }) + + let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count)) + + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { value in + guard let strongSelf = self else { + return false + } + if value == .commit { + let presentationData = strongSelf.presentationData + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.8, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal = strongSelf.context.engine.peers.removeForumChannelThreads(id: peerId, threadIds: Array(threadIds)) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start() + + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for threadId in threadIds { + state.selectedThreadIds.remove(threadId) + } + return state + }) + + return true + } else if value == .undo { + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for threadId in threadIds { + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) + } + return state + }) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first)) + return true + } + return false + }), in: .current) + + strongSelf.donePressed() + })) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.present(actionSheet, in: .window(.root)) } else if !peerIds.isEmpty { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] @@ -3017,7 +2998,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - strongSelf.chatListDisplayNode.containerNode.updateState(onlyCurrent: false, { state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in var state = state for peerId in peerIds { state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) @@ -3055,7 +3036,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = (signal |> deliverOnMainQueue).start() - strongSelf.chatListDisplayNode.containerNode.updateState(onlyCurrent: false, { state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in var state = state for peerId in peerIds { state.selectedPeerIds.remove(peerId) @@ -3065,15 +3046,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true } else if value == .undo { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) - strongSelf.chatListDisplayNode.containerNode.updateState(onlyCurrent: false, { state in + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in var state = state for peerId in peerIds { state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) } return state }) - self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) return true } return false @@ -3093,7 +3074,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.present(actionSheet, in: .window(.root)) } } else if case .middle = action { - switch self.location { + switch self.chatListDisplayNode.effectiveContainerNode.location { case let .chatList(groupId): if !peerIds.isEmpty { if groupId == .root { @@ -3101,13 +3082,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.archiveChats(peerIds: Array(peerIds)) } else { if !peerIds.isEmpty { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) let _ = (self.context.engine.peers.updatePeersGroupIdInteractively(peerIds: Array(peerIds), groupId: .root) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) strongSelf.donePressed() }) } @@ -3176,7 +3157,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.updateState { state in + strongSelf.chatListDisplayNode.mainContainerNode.updateState { state in var state = state if updatedValue { state.hiddenItemShouldBeTemporaryRevealed = false @@ -3215,7 +3196,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } func hidePsa(_ id: PeerId) { - self.chatListDisplayNode.containerNode.updateState { state in + self.chatListDisplayNode.mainContainerNode.updateState { state in var state = state state.hiddenPsaPeerId = id state.peerIdWithRevealedOptions = nil @@ -3345,7 +3326,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingClearHistoryPeerIds.insert(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state @@ -3366,7 +3347,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingClearHistoryPeerIds.remove(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state @@ -3374,7 +3355,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) return true } else if value == .undo { - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingClearHistoryPeerIds.remove(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state @@ -3570,12 +3551,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } func selectPeerThread(peerId: EnginePeer.Id, threadId: Int64) { - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.selectedThreadIds.insert(threadId) return state }) - self.chatListDisplayNode.containerNode.didBeginSelectingChats?() + self.chatListDisplayNode.effectiveContainerNode.didBeginSelectingChats?() } private func commitDeletePeerThread(peerId: EnginePeer.Id, threadId: Int64, completion: @escaping () -> Void) { @@ -3586,7 +3567,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) return state @@ -3599,21 +3581,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } if value == .commit { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) let _ = self.context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).start(completed: { [weak self] in guard let self else { return } - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) return state }) - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) }) - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.selectedThreadIds.remove(threadId) return state @@ -3622,13 +3604,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion() return true } else if value == .undo { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) return state }) - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) return true } return false @@ -3647,7 +3629,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.actionDisposables.add((self.context.engine.peers.setForumChannelTopicHidden(id: peerId, threadId: threadId, isHidden: isHidden) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { - strongSelf.chatListDisplayNode.containerNode.updateState { state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false return state @@ -3767,7 +3749,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } let engine = self.context.engine - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds[0], threadId: nil)) + self.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds[0], threadId: nil)) let _ = (ApplicationSpecificNotice.incrementArchiveChatTips(accountManager: self.context.sharedContext.accountManager, count: 1) |> deliverOnMainQueue).start(next: { [weak self] previousHintCount in let _ = (engine.peers.updatePeersGroupIdInteractively(peerIds: peerIds, groupId: .archive) @@ -3775,7 +3757,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(nil) for peerId in peerIds { deleteSendMessageIntents(peerId: peerId) @@ -3786,13 +3768,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } if value == .undo { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds[0], threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds[0], threadId: nil)) let _ = (engine.peers.updatePeersGroupIdInteractively(peerIds: peerIds, groupId: .root) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) }) return true } else { @@ -3838,13 +3820,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let peerId = peer.peerId - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) - self.chatListDisplayNode.containerNode.updateState({ state in + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + self.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state }) - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) let statusText: String if case let .channel(channel) = chatPeer { if deleteGloballyIfPossible { @@ -3888,7 +3870,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } if value == .commit { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) if case let .channel(channel) = chatPeer { strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId) } @@ -3896,17 +3878,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state }) - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) deleteSendMessageIntents(peerId: peerId) }) - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.selectedPeerIds.remove(peerId) return state @@ -3915,13 +3897,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion() return true } else if value == .undo { - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) - strongSelf.chatListDisplayNode.containerNode.updateState({ state in + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in var state = state state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.peerId, threadId: nil)) return state }) - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingItemId(nil) + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil) return true } return false @@ -3929,7 +3911,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } override public func setToolbar(_ toolbar: Toolbar?, transition: ContainedViewLayoutTransition) { - if case .chatList(.root) = self.location { + if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location { super.setToolbar(toolbar, transition: transition) } else { self.chatListDisplayNode.toolbar = toolbar @@ -3946,10 +3928,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func openFilterSettings() { - self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(false) + self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false) if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController.pushViewController(chatListFilterPresetListController(context: self.context, mode: .modal, dismissed: { [weak self] in - self?.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(true) + self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true) })) } } @@ -3993,7 +3975,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) - if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter != nil { + if strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter != nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_FolderAllChats, icon: { theme in return nil }, action: { c, f in @@ -4195,17 +4177,21 @@ private final class ChatListLocationContext { return result } + private(set) var toolbar: Toolbar? + private let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) private var didSetReady: Bool = false let ready = Promise() + private var stateDisposable: Disposable? + init( context: AccountContext, location: ChatListControllerLocation, parentController: ChatListControllerImpl, hideNetworkActivityStatus: Bool, - chatListDisplayNode: ChatListControllerNode, + containerNode: ChatListContainerNode, isReorderingTabs: Signal ) { self.context = context @@ -4258,7 +4244,7 @@ private final class ChatListLocationContext { context.account.networkState, hasProxy, passcode, - chatListDisplayNode.containerNode.currentItemState, + containerNode.currentItemState, isReorderingTabs, peerStatus, parentController.updatedPresentationData.1 @@ -4319,7 +4305,7 @@ private final class ChatListLocationContext { self.titleDisposable = (combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, - chatListDisplayNode.containerNode.currentItemState, + containerNode.currentItemState, parentController.updatedPresentationData.1 ) |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, stateAndFilterId, presentationData in @@ -4335,6 +4321,145 @@ private final class ChatListLocationContext { ) }) } + + let context = self.context + let location = self.location + let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set, Set)?, NoError> = containerNode.currentItemState + |> map { state, filterId -> (Set, Set, Int32?)? in + if !state.editing { + return nil + } + return (state.selectedPeerIds, state.selectedThreadIds, filterId) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs?.0 != rhs?.0 { + return false + } + if lhs?.1 != rhs?.1 { + return false + } + if lhs?.2 != rhs?.2 { + return false + } + return true + }) + |> mapToSignal { selectedPeerIdsAndFilterId -> Signal<(ChatListSelectionOptions, Set, Set)?, NoError> in + if let (selectedPeerIds, selectedThreadIds, filterId) = selectedPeerIdsAndFilterId { + switch location { + case .chatList: + return chatListSelectionOptions(context: context, peerIds: selectedPeerIds, filterId: filterId) + |> map { options -> (ChatListSelectionOptions, Set, Set)? in + return (options, selectedPeerIds, selectedThreadIds) + } + case let .forum(peerId): + return forumSelectionOptions(context: context, peerId: peerId, threadIds: selectedThreadIds) + |> map { options -> (ChatListSelectionOptions, Set, Set)? in + return (options, selectedPeerIds, selectedThreadIds) + } + } + + } else { + return .single(nil) + } + } + + let peerView: Signal + if case let .forum(peerId) = location { + peerView = context.account.viewTracker.peerView(peerId) + |> map(Optional.init) + } else { + peerView = .single(nil) + } + + let previousToolbarValue = Atomic(value: nil) + self.stateDisposable = combineLatest(queue: .mainQueue(), + parentController.updatedPresentationData.1, + peerIdsAndOptions, + peerView + ).start(next: { [weak self, weak containerNode] presentationData, peerIdsAndOptions, peerView in + guard let strongSelf = self, let containerNode = containerNode, let parentController = strongSelf.parentController else { + return + } + var toolbar: Toolbar? + if let (options, peerIds, _) = peerIdsAndOptions { + if case .chatList(.root) = location { + let leftAction: ToolbarAction + switch options.read { + case let .all(enabled): + leftAction = ToolbarAction(title: presentationData.strings.ChatList_ReadAll, isEnabled: enabled) + case let .selective(enabled): + leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) + } + var archiveEnabled = options.delete + var displayArchive = true + if let filter = containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { + if !data.excludeArchived { + displayArchive = false + } + } + if archiveEnabled { + for peerId in peerIds { + if peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) { + archiveEnabled = false + break + } else if peerId == strongSelf.context.account.peerId { + archiveEnabled = false + break + } + } + } + toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: displayArchive ? ToolbarAction(title: presentationData.strings.ChatList_ArchiveAction, isEnabled: archiveEnabled) : nil) + } else if case .forum = strongSelf.location { + let leftAction: ToolbarAction + switch options.read { + case .all: + leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false) + case let .selective(enabled): + leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) + } + toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: nil) + } else { + let middleAction = ToolbarAction(title: presentationData.strings.ChatList_UnarchiveAction, isEnabled: !peerIds.isEmpty) + let leftAction: ToolbarAction + switch options.read { + case .all: + leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false) + case let .selective(enabled): + leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled) + } + toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: middleAction) + } + } else if let peerView = peerView, let channel = peerView.peers[peerView.peerId] as? TelegramChannel { + switch channel.participationStatus { + case .member: + toolbar = nil + default: + let actionTitle: String + if channel.flags.contains(.requestToJoin) { + actionTitle = presentationData.strings.Group_ApplyToJoin + } else { + actionTitle = presentationData.strings.Channel_JoinChannel + } + toolbar = Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: actionTitle, isEnabled: true)) + } + } + var transition: ContainedViewLayoutTransition = .immediate + let previousToolbar = previousToolbarValue.swap(toolbar) + if (previousToolbar == nil) != (toolbar == nil) { + transition = .animated(duration: 0.4, curve: .spring) + } + if strongSelf.toolbar != toolbar { + strongSelf.toolbar = toolbar + if parentController.effectiveContext === strongSelf { + parentController.setToolbar(toolbar, transition: transition) + } + } + }) + } + + deinit { + self.titleDisposable?.dispose() + self.stateDisposable?.dispose() } private func updateChatList( @@ -4625,8 +4750,4 @@ private final class ChatListLocationContext { self.parentController?.requestUpdateHeaderContent(transition: .immediate) } } - - deinit { - self.titleDisposable?.dispose() - } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 4615477753..8eed927a5d 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -253,13 +253,17 @@ private final class ChatListShimmerNode: ASDisplayNode { context.setFillColor(UIColor.clear.cgColor) if !isInlineMode { - if itemNodes[sampleIndex].avatarNode.isHidden { + if !itemNodes[sampleIndex].avatarNode.isHidden { context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.frame.offsetBy(dx: 0.0, dy: currentY)) } } let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + if isInlineMode { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0) + } else { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + } let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) @@ -1157,7 +1161,15 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - let containerNode: ChatListContainerNode + let mainContainerNode: ChatListContainerNode + + var effectiveContainerNode: ChatListContainerNode { + if let inlineStackContainerNode = self.inlineStackContainerNode { + return inlineStackContainerNode + } else { + return self.mainContainerNode + } + } private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0 private(set) var inlineStackContainerNode: ChatListContainerNode? @@ -1206,7 +1218,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { var filterBecameEmpty: ((ChatListFilter?) -> Void)? var filterEmptyAction: ((ChatListFilter?) -> Void)? var secondaryEmptyAction: (() -> Void)? - self.containerNode = ChatListContainerNode(context: context, location: location, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, isInlineMode: false, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in + self.mainContainerNode = ChatListContainerNode(context: context, location: location, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, isInlineMode: false, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in filterBecameEmpty?(filter) }, filterEmptyAction: { filter in filterEmptyAction?(filter) @@ -1224,12 +1236,12 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.backgroundColor = presentationData.theme.chatList.backgroundColor - self.addSubnode(self.containerNode) + self.addSubnode(self.mainContainerNode) - self.containerNode.contentOffsetChanged = { [weak self] offset in + self.mainContainerNode.contentOffsetChanged = { [weak self] offset in self?.contentOffsetChanged(offset: offset, isPrimary: true) } - self.containerNode.contentScrollingEnded = { [weak self] listView in + self.mainContainerNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded(listView: listView, isPrimary: true) ?? false } @@ -1259,7 +1271,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { (controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false) } - self.containerNode.onFilterSwitch = { [weak self] in + self.mainContainerNode.onFilterSwitch = { [weak self] in if let strongSelf = self { strongSelf.controller?.dismissAllUndoControllers() } @@ -1362,7 +1374,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - self.containerNode.updatePresentationData(presentationData) + self.mainContainerNode.updatePresentationData(presentationData) self.inlineStackContainerNode?.updatePresentationData(presentationData) self.searchDisplayController?.updatePresentationData(presentationData) @@ -1432,7 +1444,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { childrenLayout.intrinsicInsets = UIEdgeInsets(top: visualNavigationHeight, left: childrenLayout.intrinsicInsets.left, bottom: childrenLayout.intrinsicInsets.bottom, right: childrenLayout.intrinsicInsets.right) self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) - transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) var mainNavigationBarHeight = navigationBarHeight var cleanMainNavigationBarHeight = cleanNavigationBarHeight var mainInsets = insets @@ -1441,13 +1453,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { cleanMainNavigationBarHeight = visualNavigationHeight mainInsets.top = visualNavigationHeight } - self.containerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, transition: transition) + self.mainContainerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, transition: transition) if let inlineStackContainerNode = self.inlineStackContainerNode { var inlineStackContainerNodeTransition = transition var animateIn = false if inlineStackContainerNode.supernode == nil { - self.insertSubnode(inlineStackContainerNode, aboveSubnode: self.containerNode) + self.insertSubnode(inlineStackContainerNode, aboveSubnode: self.mainContainerNode) inlineStackContainerNodeTransition = .immediate animateIn = true } @@ -1486,9 +1498,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location - var filter: ChatListNodePeersFilter = [] + let filter: ChatListNodePeersFilter = [] if case .forum = effectiveLocation { - filter.insert(.excludeRecent) + //filter.insert(.excludeRecent) } let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in @@ -1515,7 +1527,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { requestDeactivateSearch() } }) - self.containerNode.accessibilityElementsHidden = true + self.mainContainerNode.accessibilityElementsHidden = true + self.inlineStackContainerNode?.accessibilityElementsHidden = true return (contentNode.filterContainerNode, { [weak self] focus in guard let strongSelf = self else { @@ -1538,7 +1551,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { if let searchDisplayController = self.searchDisplayController { searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) self.searchDisplayController = nil - self.containerNode.accessibilityElementsHidden = false + self.mainContainerNode.accessibilityElementsHidden = false + self.inlineStackContainerNode?.accessibilityElementsHidden = false return { [weak self] in if let strongSelf = self, let (layout, _, _, cleanNavigationBarHeight) = strongSelf.containerLayout { @@ -1551,7 +1565,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } func clearHighlightAnimated(_ animated: Bool) { - self.containerNode.currentItemNode.clearHighlightAnimated(true) + self.mainContainerNode.currentItemNode.clearHighlightAnimated(true) self.inlineStackContainerNode?.currentItemNode.clearHighlightAnimated(true) } @@ -1581,7 +1595,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { if isPrimary { targetNode = inlineStackContainerNode } else { - targetNode = self.containerNode + targetNode = self.mainContainerNode } switch offset { @@ -1626,24 +1640,27 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return self.contentScrollingEnded?(listView) ?? false } - func setInlineChatList(location: ChatListControllerLocation?) { - if let location = location { - if self.inlineStackContainerNode?.location != location { - let inlineStackContainerNode = ChatListContainerNode(context: self.context, location: location, previewing: false, controlsHistoryPreload: false, isInlineMode: true, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { _ in }, secondaryEmptyAction: {}) - + func makeInlineChatList(location: ChatListControllerLocation) -> ChatListContainerNode { + let inlineStackContainerNode = ChatListContainerNode(context: self.context, location: location, previewing: false, controlsHistoryPreload: false, isInlineMode: true, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { _ in }, secondaryEmptyAction: {}) + return inlineStackContainerNode + } + + func setInlineChatList(inlineStackContainerNode: ChatListContainerNode?) { + if let inlineStackContainerNode = inlineStackContainerNode { + if self.inlineStackContainerNode !== inlineStackContainerNode { inlineStackContainerNode.leftSeparatorLayer.isHidden = false - inlineStackContainerNode.presentAlert = self.containerNode.presentAlert - inlineStackContainerNode.present = self.containerNode.present - inlineStackContainerNode.push = self.containerNode.push - inlineStackContainerNode.deletePeerChat = self.containerNode.deletePeerChat - inlineStackContainerNode.deletePeerThread = self.containerNode.deletePeerThread - inlineStackContainerNode.setPeerThreadStopped = self.containerNode.setPeerThreadStopped - inlineStackContainerNode.setPeerThreadPinned = self.containerNode.setPeerThreadPinned - inlineStackContainerNode.setPeerThreadHidden = self.containerNode.setPeerThreadHidden - inlineStackContainerNode.peerSelected = self.containerNode.peerSelected - inlineStackContainerNode.groupSelected = self.containerNode.groupSelected - inlineStackContainerNode.updatePeerGrouping = self.containerNode.updatePeerGrouping + inlineStackContainerNode.presentAlert = self.mainContainerNode.presentAlert + inlineStackContainerNode.present = self.mainContainerNode.present + inlineStackContainerNode.push = self.mainContainerNode.push + inlineStackContainerNode.deletePeerChat = self.mainContainerNode.deletePeerChat + inlineStackContainerNode.deletePeerThread = self.mainContainerNode.deletePeerThread + inlineStackContainerNode.setPeerThreadStopped = self.mainContainerNode.setPeerThreadStopped + inlineStackContainerNode.setPeerThreadPinned = self.mainContainerNode.setPeerThreadPinned + inlineStackContainerNode.setPeerThreadHidden = self.mainContainerNode.setPeerThreadHidden + inlineStackContainerNode.peerSelected = self.mainContainerNode.peerSelected + inlineStackContainerNode.groupSelected = self.mainContainerNode.groupSelected + inlineStackContainerNode.updatePeerGrouping = self.mainContainerNode.updatePeerGrouping inlineStackContainerNode.contentOffsetChanged = { [weak self] offset in self?.contentOffsetChanged(offset: offset, isPrimary: false) @@ -1652,9 +1669,9 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return self?.contentScrollingEnded(listView: listView, isPrimary: false) ?? false } - inlineStackContainerNode.activateChatPreview = self.containerNode.activateChatPreview - inlineStackContainerNode.addedVisibleChatsWithPeerIds = self.containerNode.addedVisibleChatsWithPeerIds - inlineStackContainerNode.didBeginSelectingChats = nil + inlineStackContainerNode.activateChatPreview = self.mainContainerNode.activateChatPreview + inlineStackContainerNode.addedVisibleChatsWithPeerIds = self.mainContainerNode.addedVisibleChatsWithPeerIds + inlineStackContainerNode.didBeginSelectingChats = self.mainContainerNode.didBeginSelectingChats inlineStackContainerNode.displayFilterLimit = nil let previousInlineStackContainerNode = self.inlineStackContainerNode @@ -1663,7 +1680,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.inlineStackContainerTransitionFraction = 1.0 if let _ = self.containerLayout { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring) if let previousInlineStackContainerNode { transition.updatePosition(node: previousInlineStackContainerNode, position: CGPoint(x: previousInlineStackContainerNode.position.x + previousInlineStackContainerNode.bounds.width + UIScreenPixel, y: previousInlineStackContainerNode.position.y), completion: { [weak previousInlineStackContainerNode] _ in @@ -1681,7 +1698,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.inlineStackContainerNode = nil self.inlineStackContainerTransitionFraction = 0.0 - self.containerNode.contentScrollingEnded = self.contentScrollingEnded + self.mainContainerNode.contentScrollingEnded = self.contentScrollingEnded if let _ = self.containerLayout { let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) @@ -1701,7 +1718,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } func playArchiveAnimation() { - self.containerNode.playArchiveAnimation() + self.mainContainerNode.playArchiveAnimation() } func scrollToTop() { @@ -1710,7 +1727,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } else if let inlineStackContainerNode = self.inlineStackContainerNode { inlineStackContainerNode.scrollToTop() } else { - self.containerNode.scrollToTop() + self.mainContainerNode.scrollToTop() } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 7c3c8fd75f..9c546a906e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -136,6 +136,11 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { + var initialFilter = initialFilter + if case .chats = initialFilter, case .forum = location { + initialFilter = .topics + } + self.context = context self.peersFilter = filter self.location = location diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 2c0e41c32e..23522aaacf 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -720,7 +720,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openClearRecentlyDownloaded() }) case .index: - header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + var headerType: ChatListSearchItemHeaderType = .messages(location: nil) + if case .forum = location, let peer = peer.peer { + headerType = .messages(location: peer.compactDisplayTitle) + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none var isMedia = false @@ -980,7 +984,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var peersFilter = peersFilter if case .forum = location { - peersFilter.insert(.excludeRecent) + //peersFilter.insert(.excludeRecent) } else if case .chatList(.archive) = location { peersFilter.insert(.excludeRecent) } @@ -1106,7 +1110,21 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let previousRecentlySearchedPeerOrder = Atomic<[EnginePeer.Id]>(value: []) let fixedRecentlySearchedPeers: Signal<[RecentlySearchedPeer], NoError> - if case .chats = key, case .chatList(.root) = location { + + var enableRecentlySearched = false + if !self.peersFilter.contains(.excludeRecent) { + if case .chats = key { + if case .chatList(.root) = location { + enableRecentlySearched = true + } else if case .forum = location { + enableRecentlySearched = true + } + } else if case .topics = key, case .forum = location { + enableRecentlySearched = true + } + } + + if enableRecentlySearched { fixedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() |> map { peers -> [RecentlySearchedPeer] in var result: [RecentlySearchedPeer] = [] @@ -1283,7 +1301,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> - if let query = query, case .chats = key { + if let query = query, (key == .chats || key == .topics) { let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() |> map { peers -> [RecentlySearchedPeer] in let allIds = peers.map(\.peer.peerId) @@ -3071,6 +3089,11 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }) + var isInlineMode = false + if case .topics = key { + isInlineMode = true + } + interaction.isInlineMode = isInlineMode let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { @@ -3277,17 +3300,36 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let selectionOffset: CGFloat = hasSelection ? 45.0 : 0.0 if let itemNode = itemNodes[sampleIndex] as? ChatListItemNode { - context.fillEllipse(in: itemNode.avatarNode.frame.offsetBy(dx: 0.0, dy: currentY)) + if !isInlineMode { + if !itemNode.avatarNode.isHidden { + context.fillEllipse(in: itemNode.avatarNode.frame.offsetBy(dx: 0.0, dy: currentY)) + } + } + let titleFrame = itemNode.titleNode.frame.offsetBy(dx: 0.0, dy: currentY) - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + if isInlineMode { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0) + } else { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + } - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: currentY + itemHeight - floor(itemNode.titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) + let textFrame = itemNode.textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) + if isInlineMode { + context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0))) + } + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNode.titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) let dateFrame = itemNode.dateNode.frame.offsetBy(dx: 0.0, dy: currentY) - fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: floor(dateFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 30.0) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNode.separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) context.setBlendMode(.normal) context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) diff --git a/submodules/ChatListUI/Sources/ChatListSelection.swift b/submodules/ChatListUI/Sources/ChatListSelection.swift index 36adf5dfae..37a39d649b 100644 --- a/submodules/ChatListUI/Sources/ChatListSelection.swift +++ b/submodules/ChatListUI/Sources/ChatListSelection.swift @@ -58,32 +58,31 @@ func chatListSelectionOptions(context: AccountContext, peerIds: Set, fil } -func forumSelectionOptions(context: AccountContext, peerId: PeerId, threadIds: Set, canDelete: Bool) -> Signal { - let threadIdsArray = Array(threadIds) - - var threadSignals: [Signal] = [] - for threadId in threadIdsArray { - threadSignals.append( - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: threadId)) - ) - } - - return combineLatest(threadSignals) - |> map { threadDatas -> ChatListSelectionOptions in - if threadIds.isEmpty { +func forumSelectionOptions(context: AccountContext, peerId: PeerId, threadIds: Set) -> Signal { + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + EngineDataList(threadIds.map { TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: $0) }) + ) + |> map { peer, threadDatas -> ChatListSelectionOptions in + guard !threadIds.isEmpty, case let .channel(channel) = peer else { return ChatListSelectionOptions(read: .selective(enabled: false), delete: false) - } else { - var hasUnread = false - for thread in threadDatas { - guard let thread = thread else { - continue - } - if thread.incomingUnreadCount > 0 { - hasUnread = true - break - } - } - return ChatListSelectionOptions(read: .selective(enabled: hasUnread), delete: canDelete) } + + var canDelete = !threadIds.contains(1) + if !channel.hasPermission(.deleteAllMessages) { + canDelete = false + } + + var hasUnread = false + for thread in threadDatas { + guard let thread = thread else { + continue + } + if thread.incomingUnreadCount > 0 { + hasUnread = true + break + } + } + return ChatListSelectionOptions(read: .selective(enabled: hasUnread), delete: canDelete) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 42fb63b3e5..6cc1639b43 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -104,8 +104,12 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { switch self.index { case let .chatList(index): return index.pinningIndex != nil - case .forum: - return false + case let .forum(pinnedIndex, _, _, _, _): + if case .index = pinnedIndex { + return true + } else { + return false + } } } @@ -1349,9 +1353,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateAlpha(layer: compoundHighlightingNode.layer, alpha: self.authorNode.alpha) } - if let item = self.item, case let .chatList(index) = item.index { + if let item = self.item { let onlineIcon: UIImage? - if index.pinningIndex != nil { + if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) @@ -2015,7 +2019,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundReactions(item.presentationData.theme, diameter: badgeDiameter) } mentionBadgeContent = .mention - } else if case let .chatList(chatListIndex) = item.index, chatListIndex.pinningIndex != nil, promoInfo == nil, currentBadgeBackgroundImage == nil { + } else if item.isPinned, promoInfo == nil, currentBadgeBackgroundImage == nil { currentPinnedIconImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme, diameter: badgeDiameter) } } @@ -2896,6 +2900,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let compoundTextButtonNode = strongSelf.compoundTextButtonNode { if strongSelf.textNode.textNode.supernode !== compoundTextButtonNode { compoundTextButtonNode.addSubnode(strongSelf.textNode.textNode) + if let dustNode = strongSelf.dustNode { + compoundTextButtonNode.addSubnode(dustNode) + } } strongSelf.textNode.textNode.frame = textNodeFrame.offsetBy(dx: -compoundTextButtonNode.frame.minX, dy: -compoundTextButtonNode.frame.minY) @@ -2903,6 +2910,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { if strongSelf.textNode.textNode.supernode !== strongSelf.mainContentContainerNode { strongSelf.mainContentContainerNode.addSubnode(strongSelf.textNode.textNode) + if let dustNode = strongSelf.dustNode { + strongSelf.mainContentContainerNode.addSubnode(dustNode) + } } strongSelf.textNode.textNode.frame = textNodeFrame @@ -2917,7 +2927,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { dustNode = InvisibleInkDustNode(textNode: nil) dustNode.isUserInteractionEnabled = false strongSelf.dustNode = dustNode - strongSelf.mainContentContainerNode.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode.textNode) + + strongSelf.textNode.textNode.supernode?.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode.textNode) } dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, textColor: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index da6540e885..b93d1a6e4c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -469,10 +469,17 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL var isForum = false if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) { isForum = true - if editing { + if editing, case .chatList = mode { enabled = false } } + + var selectable = editing + if case .chatList = mode { + if isForum { + selectable = false + } + } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), @@ -483,7 +490,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL peer: peerContent, status: status, enabled: enabled, - selection: editing && !isForum ? .selectable(selected: selected) : .none, + selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, @@ -644,11 +651,18 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL var isForum = false if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) { isForum = true - if editing { + if editing, case .chatList = mode { enabled = false } } + var selectable = editing + if case .chatList = mode { + if isForum { + selectable = false + } + } + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, @@ -658,7 +672,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL peer: peerContent, status: status, enabled: enabled, - selection: editing && !isForum ? .selectable(selected: selected) : .none, + selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index e0e9e08d9b..1a13d3f84e 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1631,7 +1631,7 @@ open class NavigationBar: ASDisplayNode { if self.secondaryContentNode !== secondaryContentNode { if let previous = self.secondaryContentNode, previous.supernode === self.clippingNode { if animated { - previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previous] finished in + previous.layer.animateAlpha(from: previous.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previous] finished in if finished { previous?.removeFromSupernode() previous?.layer.removeAllAnimations() @@ -1646,7 +1646,7 @@ open class NavigationBar: ASDisplayNode { self.clippingNode.addSubnode(secondaryContentNode) if animated { - secondaryContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + secondaryContentNode.layer.animateAlpha(from: 0.0, to: secondaryContentNode.alpha, duration: 0.3) } } } diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 789cd19ce2..df3e542697 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -107,6 +107,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/TelegramUI/Components/NotificationPeerExceptionController", + "//submodules/TelegramUI/Components/ChatTimerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index ff19fc0f1d..d355f62b3e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -18,6 +18,7 @@ import UndoUI import PremiumUI import AuthorizationUI import AuthenticationServices +import ChatTimerScreen private final class PrivacyAndSecurityControllerArguments { let account: Account @@ -34,10 +35,11 @@ private final class PrivacyAndSecurityControllerArguments { let openActiveSessions: () -> Void let toggleArchiveAndMuteNonContacts: (Bool) -> Void let setupAccountAutoremove: () -> Void + let setupMessageAutoremove: () -> Void let openDataSettings: () -> Void let openEmailSettings: (String?) -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -52,6 +54,7 @@ private final class PrivacyAndSecurityControllerArguments { self.openActiveSessions = openActiveSessions self.toggleArchiveAndMuteNonContacts = toggleArchiveAndMuteNonContacts self.setupAccountAutoremove = setupAccountAutoremove + self.setupMessageAutoremove = setupMessageAutoremove self.openDataSettings = openDataSettings self.openEmailSettings = openEmailSettings } @@ -62,11 +65,13 @@ private enum PrivacyAndSecuritySection: Int32 { case privacy case autoArchive case account + case messageAutoremove case dataSettings } public enum PrivacyAndSecurityEntryTag: ItemListItemTag { case accountTimeout + case messageAutoremoveTimeout case autoArchive public func isEqual(to other: ItemListItemTag) -> Bool { @@ -100,6 +105,9 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case accountHeader(PresentationTheme, String) case accountTimeout(PresentationTheme, String, String) case accountInfo(PresentationTheme, String) + case messageAutoremoveHeader(PresentationTheme, String) + case messageAutoremoveTimeout(PresentationTheme, String, String) + case messageAutoremoveInfo(PresentationTheme, String) case dataSettings(PresentationTheme, String) case dataSettingsInfo(PresentationTheme, String) @@ -113,6 +121,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return PrivacyAndSecuritySection.autoArchive.rawValue case .accountHeader, .accountTimeout, .accountInfo: return PrivacyAndSecuritySection.account.rawValue + case .messageAutoremoveHeader, .messageAutoremoveTimeout, .messageAutoremoveInfo: + return PrivacyAndSecuritySection.messageAutoremove.rawValue case .dataSettings, .dataSettingsInfo: return PrivacyAndSecuritySection.dataSettings.rawValue } @@ -162,10 +172,16 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 20 case .accountInfo: return 21 - case .dataSettings: + case .messageAutoremoveHeader: return 22 - case .dataSettingsInfo: + case .messageAutoremoveTimeout: return 23 + case .messageAutoremoveInfo: + return 24 + case .dataSettings: + return 25 + case .dataSettingsInfo: + return 26 } } @@ -297,6 +313,24 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .messageAutoremoveHeader(lhsTheme, lhsText): + if case let .messageAutoremoveHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .messageAutoremoveTimeout(lhsTheme, lhsText, lhsValue): + if case let .messageAutoremoveTimeout(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .messageAutoremoveInfo(lhsTheme, lhsText): + if case let .messageAutoremoveInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .dataSettings(lhsTheme, lhsText): if case let .dataSettings(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -389,6 +423,14 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }, tag: PrivacyAndSecurityEntryTag.accountTimeout) case let .accountInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .messageAutoremoveHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .messageAutoremoveTimeout(_, text, value): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.setupMessageAutoremove() + }, tag: PrivacyAndSecurityEntryTag.messageAutoremoveTimeout) + case let .messageAutoremoveInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .dataSettings(_, text): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openDataSettings() @@ -402,6 +444,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { private struct PrivacyAndSecurityControllerState: Equatable { var updatingAccountTimeoutValue: Int32? = nil var updatingAutomaticallyArchiveAndMuteNonContacts: Bool? = nil + var updatingMessageAutoremoveTimeoutValue: Int32? = nil } private func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> Int { @@ -545,6 +588,27 @@ private func privacyAndSecurityControllerEntries( } entries.append(.accountInfo(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountHelp)) + //TODO:localize + entries.append(.messageAutoremoveHeader(presentationData.theme, "MESSAGE TIMER")) + if let privacySettings = privacySettings { + let value: Int32? + if let updatingMessageAutoremoveTimeoutValue = state.updatingMessageAutoremoveTimeoutValue { + value = updatingMessageAutoremoveTimeoutValue + } else { + value = privacySettings.messageAutoremoveTimeout + } + let valueText: String + if let value { + valueText = timeIntervalString(strings: presentationData.strings, value: value) + } else { + valueText = "Never" + } + entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", valueText)) + } else { + entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", presentationData.strings.Channel_NotificationLoading)) + } + entries.append(.messageAutoremoveInfo(presentationData.theme, "Set a global message timer.")) + entries.append(.dataSettings(presentationData.theme, presentationData.strings.PrivacySettings_DataSettings)) entries.append(.dataSettingsInfo(presentationData.theme, presentationData.strings.PrivacySettings_DataSettingsHelp)) @@ -680,7 +744,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -703,7 +767,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -740,7 +804,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -763,7 +827,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -786,7 +850,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -809,7 +873,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -839,7 +903,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: updated, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: updated, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -912,7 +976,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: archiveValue, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: archiveValue, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -951,7 +1015,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -999,6 +1063,85 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting presentControllerImpl?(controller) } })) + }, setupMessageAutoremove: { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in + if let privacySettingsValue = privacySettingsValue { + let timeoutAction: (Int32?) -> Void = { timeout in + if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable { + updateState { state in + var state = state + state.updatingMessageAutoremoveTimeoutValue = timeout + return state + } + let applyTimeout: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: timeout))) + } + return .complete() + } + updateAccountTimeoutDisposable.set((context.engine.privacy.updateGlobalMessageRemovalTimeout(timeout: timeout) + |> then(applyTimeout) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state.updatingMessageAutoremoveTimeoutValue = nil + return state + } + })) + } + } + + let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: privacySettingsValue.messageAutoremoveTimeout, dismissByTapOutside: true, completion: { value in + timeoutAction(value) + }) + presentControllerImpl?(controller) + + /* + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let timeoutValues: [Int32] = [ + 1 * 30 * 24 * 60 * 60, + 3 * 30 * 24 * 60 * 60, + 6 * 30 * 24 * 60 * 60, + 365 * 24 * 60 * 60 + ] + var timeoutItems: [ActionSheetItem] = timeoutValues.map { value in + return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value), action: { + dismissAction() + timeoutAction(value) + }) + } + timeoutItems.append(ActionSheetButtonItem(title: presentationData.strings.PrivacySettings_DeleteAccountNow, color: .destructive, action: { + dismissAction() + + guard let navigationController = getNavigationControllerImpl?() else { + return + } + + let _ = (combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get()) + |> take(1) + |> deliverOnMainQueue).start(next: { hasTwoStepAuth, twoStepAuthData in + let optionsController = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) + pushControllerImpl?(optionsController, true) + }) + })) + controller.setItemGroups([ + ActionSheetItemGroup(items: timeoutItems), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller)*/ + } + })) }, openDataSettings: { pushControllerImpl?(dataPrivacyController(context: context), true) }, openEmailSettings: { emailPattern in diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 539a285929..eca711ed97 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -180,6 +180,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[383348795] = { return Api.ContactStatus.parse_contactStatus($0) } dict[2104790276] = { return Api.DataJSON.parse_dataJSON($0) } dict[414687501] = { return Api.DcOption.parse_dcOption($0) } + dict[1135897376] = { return Api.DefaultHistoryTTL.parse_defaultHistoryTTL($0) } dict[-1460809483] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } dict[1949890536] = { return Api.DialogFilter.parse_dialogFilter($0) } @@ -1255,6 +1256,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.DcOption: _1.serialize(buffer, boxed) + case let _1 as Api.DefaultHistoryTTL: + _1.serialize(buffer, boxed) case let _1 as Api.Dialog: _1.serialize(buffer, boxed) case let _1 as Api.DialogFilter: diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 3499b5f17a..f5443acdc0 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1852,15 +1852,16 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func createChannel(flags: Int32, title: String, about: String, geoPoint: Api.InputGeoPoint?, address: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func createChannel(flags: Int32, title: String, about: String, geoPoint: Api.InputGeoPoint?, address: String?, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1029681423) + buffer.appendInt32(-1862244601) serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) serializeString(about, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {geoPoint!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeString(address!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "channels.createChannel", parameters: [("flags", String(describing: flags)), ("title", String(describing: title)), ("about", String(describing: about)), ("geoPoint", String(describing: geoPoint)), ("address", String(describing: address))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "channels.createChannel", parameters: [("flags", String(describing: flags)), ("title", String(describing: title)), ("about", String(describing: about)), ("geoPoint", String(describing: geoPoint)), ("address", String(describing: address)), ("ttlPeriod", String(describing: ttlPeriod))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -3725,16 +3726,18 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func createChat(users: [Api.InputUser], title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func createChat(flags: Int32, users: [Api.InputUser], title: String, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(164303470) + buffer.appendInt32(3450904) + serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) for item in users { item.serialize(buffer, true) } serializeString(title, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.createChat", parameters: [("users", String(describing: users)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.createChat", parameters: [("flags", String(describing: flags)), ("users", String(describing: users)), ("title", String(describing: title)), ("ttlPeriod", String(describing: ttlPeriod))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -4351,6 +4354,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getDefaultHistoryTTL() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1703637384) + + return (FunctionDescription(name: "messages.getDefaultHistoryTTL", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.DefaultHistoryTTL? in + let reader = BufferReader(buffer) + var result: Api.DefaultHistoryTTL? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.DefaultHistoryTTL + } + return result + }) + } +} public extension Api.functions.messages { static func getDhConfig(version: Int32, randomLength: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6335,6 +6353,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func setDefaultHistoryTTL(period: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1632299963) + serializeInt32(period, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.setDefaultHistoryTTL", parameters: [("period", String(describing: period))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func setDefaultReaction(reaction: Api.Reaction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index a382f35afc..052c017a21 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -836,6 +836,42 @@ public extension Api { } } +public extension Api { + enum DefaultHistoryTTL: TypeConstructorDescription { + case defaultHistoryTTL(period: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .defaultHistoryTTL(let period): + if boxed { + buffer.appendInt32(1135897376) + } + serializeInt32(period, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .defaultHistoryTTL(let period): + return ("defaultHistoryTTL", [("period", String(describing: period))]) + } + } + + public static func parse_defaultHistoryTTL(_ reader: BufferReader) -> DefaultHistoryTTL? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.DefaultHistoryTTL.defaultHistoryTTL(period: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum Dialog: TypeConstructorDescription { case dialog(flags: Int32, peer: Api.Peer, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, notifySettings: Api.PeerNotifySettings, pts: Int32?, draft: Api.DraftMessage?, folderId: Int32?) diff --git a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift index 0ecaca1281..9446cd790e 100644 --- a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift @@ -97,8 +97,9 @@ public struct AccountPrivacySettings: Equatable { public let automaticallyArchiveAndMuteNonContacts: Bool public let accountRemovalTimeout: Int32 + public let messageAutoremoveTimeout: Int32? - public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, voiceMessages: SelectivePrivacySettings, automaticallyArchiveAndMuteNonContacts: Bool, accountRemovalTimeout: Int32) { + public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, voiceMessages: SelectivePrivacySettings, automaticallyArchiveAndMuteNonContacts: Bool, accountRemovalTimeout: Int32, messageAutoremoveTimeout: Int32?) { self.presence = presence self.groupInvitations = groupInvitations self.voiceCalls = voiceCalls @@ -110,6 +111,7 @@ public struct AccountPrivacySettings: Equatable { self.voiceMessages = voiceMessages self.automaticallyArchiveAndMuteNonContacts = automaticallyArchiveAndMuteNonContacts self.accountRemovalTimeout = accountRemovalTimeout + self.messageAutoremoveTimeout = messageAutoremoveTimeout } public static func ==(lhs: AccountPrivacySettings, rhs: AccountPrivacySettings) -> Bool { @@ -146,6 +148,9 @@ public struct AccountPrivacySettings: Equatable { if lhs.accountRemovalTimeout != rhs.accountRemovalTimeout { return false } + if lhs.messageAutoremoveTimeout != rhs.messageAutoremoveTimeout { + return false + } return true } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift index 44238541e4..12ba575b96 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift @@ -34,7 +34,7 @@ private func createChannel(account: Account, title: String, description: String? transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedGroupCallDisplayAsPeers) - return account.network.request(Api.functions.channels.createChannel(flags: flags, title: title, about: description ?? "", geoPoint: geoPoint, address: address), automaticFloodWait: false) + return account.network.request(Api.functions.channels.createChannel(flags: flags, title: title, about: description ?? "", geoPoint: geoPoint, address: address, ttlPeriod: nil), automaticFloodWait: false) |> mapError { error -> CreateChannelError in if error.errorCode == 406 { return .serverProvided(error.errorDescription) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift index 4245f6cb52..ad316f9740 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift @@ -23,7 +23,7 @@ func _internal_createGroup(account: Account, title: String, peerIds: [PeerId]) - return .single(nil) } } - return account.network.request(Api.functions.messages.createChat(users: inputUsers, title: title)) + return account.network.request(Api.functions.messages.createChat(flags: 0, users: inputUsers, title: title, ttlPeriod: nil)) |> mapError { error -> CreateGroupError in if error.errorDescription == "USERS_TOO_FEW" { return .privacy diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 3e6c12fed7..df34b17cea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -866,6 +866,19 @@ public extension TelegramEngine { |> ignoreValues } + public func removeForumChannelThreads(id: EnginePeer.Id, threadIds: [Int64]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for threadId in threadIds { + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: id, threadId: threadId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(.forEveryone)) + + transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) + + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + } + } + |> ignoreValues + } + public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal { return self.account.postbox.transaction { transaction -> ([Int64], Int) in var limit = 5 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index 19533db705..9f56c06f76 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -32,6 +32,10 @@ public extension TelegramEngine { public func updateAccountRemovalTimeout(timeout: Int32) -> Signal { return _internal_updateAccountRemovalTimeout(account: self.account, timeout: timeout) } + + public func updateGlobalMessageRemovalTimeout(timeout: Int32?) -> Signal { + return _internal_updateMessageRemovalTimeout(account: self.account, timeout: timeout) + } public func updatePhoneNumberDiscovery(value: Bool) -> Signal { return _internal_updatePhoneNumberDiscovery(account: self.account, value: value) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 433cdeeda7..fc46e26ab1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -16,17 +16,29 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal `catch` { _ in return .complete() } - |> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, voiceMessagesPrivacy, autoremoveTimeout, globalPrivacySettings -> Signal in + |> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, voiceMessagesPrivacy, autoremoveTimeout, globalPrivacySettings, messageAutoremoveTimeout -> Signal in let accountTimeoutSeconds: Int32 switch autoremoveTimeout { case let .accountDaysTTL(days): accountTimeoutSeconds = days * 24 * 60 * 60 } + let messageAutoremoveSeconds: Int32? + switch messageAutoremoveTimeout { + case let .defaultHistoryTTL(period): + if period != 0 { + messageAutoremoveSeconds = period + } else { + messageAutoremoveSeconds = nil + } + } + let lastSeenRules: [Api.PrivacyRule] let groupRules: [Api.PrivacyRule] let voiceRules: [Api.PrivacyRule] @@ -143,7 +155,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal } } +func _internal_updateMessageRemovalTimeout(account: Account, timeout: Int32?) -> Signal { + return account.network.request(Api.functions.messages.setDefaultHistoryTTL(period: timeout ?? 0)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } +} + func _internal_updatePhoneNumberDiscovery(account: Account, value: Bool) -> Signal { var rules: [Api.InputPrivacyRule] = [] if value { diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 296906a2de..7a955b7d3d 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -916,12 +916,14 @@ public final class NavigationButtonComponent: Component { guard let self, let component = self.component else { return } + self.moreButton?.play() component.pressed(self) } moreButton.contextAction = { [weak self] sourceNode, gesture in guard let self, let component = self.component else { return } + self.moreButton?.play() component.contextAction?(self, gesture) } self.moreButton = moreButton diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift index ccf39e1dd8..586b4780c9 100644 --- a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift +++ b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift @@ -30,7 +30,6 @@ public final class ChatTimerScreen: ViewController { private var animatedIn = false private let context: AccountContext - private let peerId: PeerId private let style: ChatTimerScreenStyle private let mode: ChatTimerScreenMode private let currentTime: Int32? @@ -40,9 +39,8 @@ public final class ChatTimerScreen: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { self.context = context - self.peerId = peerId self.style = style self.mode = mode self.currentTime = currentTime diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b8b889c4b1..4a481382c9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -14934,7 +14934,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let navigationController = self.effectiveNavigationController { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) - if let message = message, let threadId = message.threadId { + if case let .channel(channel) = peer, channel.flags.contains(.isForum), let message = message, let threadId = message.threadId { chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } @@ -17171,10 +17171,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { - guard case let .peer(peerId) = self.chatLocation else { + guard case .peer = self.chatLocation else { return } - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peerId: peerId, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in completion(time) }) self.chatDisplayNode.dismissInput() @@ -17231,7 +17231,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peerId: peer.id, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, dismissByTapOutside: true, completion: { [weak self] value in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index bbf745b0b5..ef7b01b756 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2367,8 +2367,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return VisibleMessageRange(lowerBound: range.lowerBound, upperBound: range.upperBound) }) - if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last { + if let loaded = displayedRange.visibleRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last { if loaded.firstIndex < 5 && historyView.originalView.laterId != nil { + if !"".isEmpty { + print("load next") + return + } let locationInput: ChatHistoryLocation = .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount, highlight: false) if self.chatHistoryLocationValue?.content != locationInput { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId()) @@ -2378,6 +2382,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount, highlight: false), id: self.takeNextHistoryLocationId()) } } else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { + if !"".isEmpty { + print("load previous") + return + } let locationInput: ChatHistoryLocation = .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false) if self.chatHistoryLocationValue?.content != locationInput { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId()) diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 95a2486031..472d7f56fa 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -17,12 +17,28 @@ import ForumCreateTopicScreen public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) { for controller in params.navigationController.viewControllers.reversed() { - if let controller = controller as? ChatListControllerImpl, case let .forum(peerId) = controller.location, peer.id == peerId { - let _ = params.navigationController.popToViewController(controller, animated: params.animated) - if let activateMessageSearch = params.activateMessageSearch { - controller.activateSearch(query: activateMessageSearch.1) + var chatListController: ChatListControllerImpl? + if let controller = controller as? ChatListControllerImpl { + chatListController = controller + } else if let controller = controller as? TabBarController { + chatListController = controller.currentController as? ChatListControllerImpl + } + + if let chatListController = chatListController { + var matches = false + if case let .forum(peerId) = chatListController.location, peer.id == peerId { + matches = true + } else if case let .forum(peerId) = chatListController.effectiveLocation, peer.id == peerId { + matches = true + } + + if matches { + let _ = params.navigationController.popToViewController(controller, animated: params.animated) + if let activateMessageSearch = params.activateMessageSearch { + chatListController.activateSearch(query: activateMessageSearch.1) + } + return } - return } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 24c10943ee..74714f64e7 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -28,6 +28,7 @@ import AnimationCache import MultiAnimationRenderer import ComponentDisplayAdapters import ChatTitleView +import AppBundle enum PeerInfoHeaderButtonKey: Hashable { case message @@ -2083,6 +2084,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode + var subtitleBackgroundNode: ASDisplayNode? + var subtitleBackgroundButton: HighlightTrackingButtonNode? + var subtitleArrowNode: ASImageNode? let panelSubtitleNode: MultiScaleTextNode let nextPanelSubtitleNode: MultiScaleTextNode let usernameNodeContainer: ASDisplayNode @@ -2111,6 +2115,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { var displayPremiumIntro: ((UIView, PeerEmojiStatus?, Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError>, Bool) -> Void)? + var navigateToForum: (() -> Void)? + var navigationTransition: PeerInfoHeaderNavigationTransition? var backgroundAlpha: CGFloat = 1.0 @@ -2161,7 +2167,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.usernameNode.displaysAsynchronously = false self.buttonsContainerNode = SparseNode() - self.buttonsContainerNode.clipsToBounds = true + self.buttonsContainerNode.clipsToBounds = false self.regularContentNode = PeerInfoHeaderRegularContentNode() var requestUpdateLayoutImpl: (() -> Void)? @@ -2300,6 +2306,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + @objc private func subtitleBackgroundPressed() { + self.navigateToForum?() + } + func invokeDisplayPremiumIntro() { self.displayPremiumIntro?(self.isAvatarExpanded ? self.titleExpandedCredibilityIconView : self.titleCredibilityIconView, nil, .never(), self.isAvatarExpanded) } @@ -2616,6 +2626,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleString: NSAttributedString let smallSubtitleString: NSAttributedString let subtitleString: NSAttributedString + var subtitleIsButton: Bool = false var panelSubtitleString: NSAttributedString? var nextPanelSubtitleString: NSAttributedString? let usernameString: NSAttributedString @@ -2659,18 +2670,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) } else if let _ = threadData { - let subtitleColor: UIColor = presentationData.theme.list.itemSecondaryTextColor + let subtitleColor: UIColor + subtitleColor = presentationData.theme.list.itemAccentColor let statusText: String - if let addressName = peer.addressName { - statusText = presentationData.strings.PeerInfo_TopicHeaderLocation("@\(addressName)").string - } else { - statusText = presentationData.strings.PeerInfo_TopicHeaderLocation(peer.debugDisplayTitle).string - } + statusText = peer.debugDisplayTitle smallSubtitleString = NSAttributedString(string: statusText, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) - subtitleString = NSAttributedString(string: statusText, font: Font.regular(17.0), textColor: subtitleColor) + subtitleString = NSAttributedString(string: statusText, font: Font.semibold(15.0), textColor: subtitleColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + + subtitleIsButton = true let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData if let panelStatusData = maybePanelStatusData { @@ -2739,6 +2749,81 @@ final class PeerInfoHeaderNode: ASDisplayNode { ], mainState: TitleNodeStateRegular) self.subtitleNode.accessibilityLabel = subtitleString.string + if subtitleIsButton { + let subtitleBackgroundNode: ASDisplayNode + if let current = self.subtitleBackgroundNode { + subtitleBackgroundNode = current + } else { + subtitleBackgroundNode = ASDisplayNode() + self.subtitleBackgroundNode = subtitleBackgroundNode + self.subtitleNode.insertSubnode(subtitleBackgroundNode, at: 0) + } + + let subtitleBackgroundButton: HighlightTrackingButtonNode + if let current = self.subtitleBackgroundButton { + subtitleBackgroundButton = current + } else { + subtitleBackgroundButton = HighlightTrackingButtonNode() + self.subtitleBackgroundButton = subtitleBackgroundButton + self.subtitleNode.addSubnode(subtitleBackgroundButton) + + subtitleBackgroundButton.addTarget(self, action: #selector(self.subtitleBackgroundPressed), forControlEvents: .touchUpInside) + subtitleBackgroundButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.subtitleNode.layer.removeAnimation(forKey: "opacity") + self.subtitleNode.alpha = 0.4 + } else { + self.subtitleNode.alpha = 1.0 + self.subtitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + let subtitleArrowNode: ASImageNode + if let current = self.subtitleArrowNode { + subtitleArrowNode = current + if themeUpdated { + subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) + } + } else { + subtitleArrowNode = ASImageNode() + self.subtitleArrowNode = subtitleArrowNode + self.subtitleNode.insertSubnode(subtitleArrowNode, at: 1) + + subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) + } + subtitleBackgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1) + let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size + var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -6.0, dy: -4.0) + subtitleBackgroundFrame.size.width += 12.0 + transition.updateFrame(node: subtitleBackgroundNode, frame: subtitleBackgroundFrame) + transition.updateCornerRadius(node: subtitleBackgroundNode, cornerRadius: subtitleBackgroundFrame.height * 0.5) + + transition.updateFrame(node: subtitleBackgroundButton, frame: subtitleBackgroundFrame) + + if let arrowImage = subtitleArrowNode.image { + let scaleFactor: CGFloat = 0.8 + let arrowSize = CGSize(width: floorToScreenPixels(arrowImage.size.width * scaleFactor), height: floorToScreenPixels(arrowImage.size.height * scaleFactor)) + subtitleArrowNode.frame = CGRect(origin: CGPoint(x: subtitleBackgroundFrame.maxX - arrowSize.width - 1.0, y: subtitleBackgroundFrame.minY + floor((subtitleBackgroundFrame.height - arrowSize.height) / 2.0)), size: arrowSize) + } + } else { + if let subtitleBackgroundNode = self.subtitleBackgroundNode { + self.subtitleBackgroundNode = nil + subtitleBackgroundNode.removeFromSupernode() + } + if let subtitleArrowNode = self.subtitleArrowNode { + self.subtitleArrowNode = nil + subtitleArrowNode.removeFromSupernode() + } + if let subtitleBackgroundButton = self.subtitleBackgroundButton { + self.subtitleBackgroundButton = nil + subtitleBackgroundButton.removeFromSupernode() + } + } + if let previousPanelStatusData = previousPanelStatusData, let currentPanelStatusData = panelStatusData.0, let previousPanelStatusDataKey = previousPanelStatusData.key, let currentPanelStatusDataKey = currentPanelStatusData.key, previousPanelStatusDataKey != currentPanelStatusDataKey { if let snapshotView = self.panelSubtitleNode.view.snapshotContentTree() { let direction: CGFloat = previousPanelStatusDataKey.rawValue > currentPanelStatusDataKey.rawValue ? 1.0 : -1.0 @@ -2803,7 +2888,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } var titleFrame: CGRect - let subtitleFrame: CGRect + var subtitleFrame: CGRect let usernameFrame: CGRect let usernameSpacing: CGFloat = 4.0 @@ -2829,6 +2914,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + if subtitleIsButton { + subtitleFrame.origin.y += 11.0 + } + let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0 let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset @@ -3025,7 +3114,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarListNode.avatarContainerNode.canAttachVideo = false } - let panelWithAvatarHeight: CGFloat = 35.0 + avatarSize + var panelWithAvatarHeight: CGFloat = 35.0 + avatarSize + if threadData != nil { + panelWithAvatarHeight += 10.0 + } let rawHeight: CGFloat let height: CGFloat @@ -3307,6 +3399,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + if let subtitleBackgroundButton = self.subtitleBackgroundButton, subtitleBackgroundButton.view.convert(subtitleBackgroundButton.bounds, to: self.view).contains(point) { + if let result = subtitleBackgroundButton.view.hitTest(self.view.convert(point, to: subtitleBackgroundButton.view), with: event) { + return result + } + } + if result.isDescendant(of: self.navigationButtonContainer.view) { return result } @@ -3314,6 +3412,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { return nil } + return result } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 47811f9687..f3ac38e59e 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3445,6 +3445,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.controller?.presentInGlobalOverlay(contextController) } + self.headerNode.navigateToForum = { [weak self] in + guard let self, let navigationController = self.controller?.navigationController as? NavigationController, let peer = self.data?.peer else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)))) + } + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peerId.namespace) { self.displayAsPeersPromise.set(context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId)) } @@ -4987,7 +4994,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private func openAutoremove(currentValue: Int32?) { - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in guard let strongSelf = self else { return } @@ -5016,7 +5023,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private func openCustomMute() { - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak self] value in guard let strongSelf = self else { return } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index e2b315ed9c..a53f4b1131 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -591,12 +591,21 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } case let .channelMessage(id, timecode): if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(id)) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .replyThread(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id)) + let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id) + return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false)) + |> take(1) + |> mapToSignal { messages -> Signal in + if let threadId = messages.first?.threadId { + return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId) + |> map { info -> ResolvedUrl? in + if let _ = info { + return .replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId) + } else { + return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + } + } } else { - return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) } } } else {