From 0a0517122c1ce0d34bc370706df152a49e6db183 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 3 Aug 2021 16:27:10 +0200 Subject: [PATCH] Update next channel navigation UI --- .../Sources/AccountContext.swift | 4 +- .../Sources/ChatListController.swift | 4 +- .../Sources/Node/ChatListNodeLocation.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 18 +++++---- .../TelegramNotices/Sources/Notices.swift | 27 ++++++++++++++ .../TelegramUI/Sources/ChatController.swift | 37 ++++++++++++++++--- .../Sources/ChatHistoryListNode.swift | 25 +++++++++---- .../Sources/ChatOverscrollControl.swift | 31 +++++++++++++--- .../Sources/NavigateToChatController.swift | 2 +- 9 files changed, 120 insertions(+), 30 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ff0845ec2c..345cf6a3a8 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -289,9 +289,10 @@ public final class NavigateToChatControllerParams { public let animated: Bool public let options: NavigationAnimationOptions public let parentGroupId: PeerGroupId? + public let chatListFilter: ChatListFilterData? public let completion: (ChatController) -> Void - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: ChatListFilterData? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -312,6 +313,7 @@ public final class NavigateToChatControllerParams { self.animated = animated self.options = options self.parentGroupId = parentGroupId + self.chatListFilter = chatListFilter self.completion = completion } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 31d12e57db..6e459e7a23 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -611,8 +611,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass { scrollToEndIfExists = true } - - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data, completion: { [weak self] controller in self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) if let promoInfo = promoInfo { switch promoInfo { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index 4e96f658ad..70909efa41 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -28,7 +28,7 @@ struct ChatListNodeViewUpdate { let scrollPosition: ChatListNodeViewScrollPosition? } -func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate { +public func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate { var includePeers = Set(filter.includePeers.peers) var excludePeers = Set(filter.excludePeers) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index d38064737a..76f87ccb93 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -495,16 +495,19 @@ public extension TelegramEngine { return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description) } - public func getNextUnreadChannel(peerId: PeerId) -> Signal { + public func getNextUnreadChannel(peerId: PeerId, filter: ChatListFilterPredicate?) -> Signal { return self.account.postbox.transaction { transaction -> EnginePeer? in - var peers: [RenderedPeer] = [] - peers.append(contentsOf: transaction.getTopChatListEntries(groupId: .root, count: 100)) - peers.append(contentsOf: transaction.getTopChatListEntries(groupId: Namespaces.PeerGroup.archive, count: 100)) - var results: [(EnginePeer, Int32)] = [] - for peer in peers { - guard let channel = peer.chatMainPeer as? TelegramChannel, case .broadcast = channel.info else { + var peerIds: [PeerId] = [] + peerIds.append(contentsOf: transaction.getUnreadChatListPeerIds(groupId: .root, filterPredicate: filter)) + peerIds.append(contentsOf: transaction.getUnreadChatListPeerIds(groupId: Namespaces.PeerGroup.archive, filterPredicate: filter)) + + for listId in peerIds { + guard let peer = transaction.getPeer(listId) else { + continue + } + guard let channel = peer as? TelegramChannel, case .broadcast = channel.info else { continue } if channel.id == peerId { @@ -516,6 +519,7 @@ public extension TelegramEngine { guard let topMessageIndex = transaction.getTopPeerMessageIndex(peerId: channel.id) else { continue } + results.append((EnginePeer(channel), topMessageIndex.timestamp)) } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index a65f6ac38f..38957258e6 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -137,6 +137,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case callsTabTip = 18 case chatFolderTips = 19 case locationProximityAlertTip = 20 + case nextChatSuggestionTip = 21 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -268,6 +269,10 @@ private struct ApplicationSpecificNoticeKeys { static func locationProximityAlertTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.locationProximityAlertTip.key) } + + static func nextChatSuggestionTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.nextChatSuggestionTip.key) + } } public struct ApplicationSpecificNotice { @@ -735,6 +740,28 @@ public struct ApplicationSpecificNotice { transaction.setNotice(ApplicationSpecificNoticeKeys.chatMessageOptionsTip(), ApplicationSpecificCounterNotice(value: currentValue)) } } + + public static func getNextChatSuggestionTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.nextChatSuggestionTip()) as? ApplicationSpecificCounterNotice { + return value.value + } else { + return 0 + } + } + } + + public static func incrementNextChatSuggestionTip(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.nextChatSuggestionTip()) as? ApplicationSpecificCounterNotice { + currentValue = value.value + } + currentValue += count + + transaction.setNotice(ApplicationSpecificNoticeKeys.nextChatSuggestionTip(), ApplicationSpecificCounterNotice(value: currentValue)) + } + } public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 416f1dd16a..b7bec869b5 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -64,6 +64,7 @@ import TelegramPermissionsUI import Speak import UniversalMediaPlayer import WallpaperBackgroundNode +import ChatListUI #if DEBUG import os.signpost @@ -212,6 +213,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var validLayout: ContainerViewLayout? public weak var parentController: ViewController? + + private let currentChatListFilter: ChatListFilterData? public var peekActions: ChatControllerPeekActions = .standard private var didSetup3dTouch: Bool = false @@ -282,6 +285,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var preloadHistoryPeerId: PeerId? private let preloadHistoryPeerIdDisposable = MetaDisposable() + + private var preloadNextChatPeerId: PeerId? + private let preloadNextChatPeerIdDisposable = MetaDisposable() private let botCallbackAlertMessage = Promise(nil) private var botCallbackAlertMessageDisposable: Disposable? @@ -459,7 +465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var nextChannelToReadDisposable: Disposable? - public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: ChatListFilterData? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 } @@ -470,6 +476,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.subject = subject self.botStart = botStart self.peekData = peekData + self.currentChatListFilter = chatListFilter var useSharedAnimationPhase = false switch mode { @@ -3306,16 +3313,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { if strongSelf.nextChannelToReadDisposable == nil { - strongSelf.nextChannelToReadDisposable = (strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id) - |> deliverOnMainQueue + strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), + strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id, filter: strongSelf.currentChatListFilter.flatMap(chatListFilterPredicate)), + ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) + ) |> then(.complete() |> delay(1.0, queue: .mainQueue())) - |> restart).start(next: { nextPeer in + |> restart).start(next: { nextPeer, nextChatSuggestionTip in guard let strongSelf = self else { return } strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer + strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + + let nextPeerId = nextPeer?.id + + if strongSelf.preloadNextChatPeerId != nextPeerId { + strongSelf.preloadNextChatPeerId = nextPeerId + if let nextPeerId = nextPeerId { + let combinedDisposable = DisposableSet() + strongSelf.preloadNextChatPeerIdDisposable.set(combinedDisposable) + combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: nextPeerId).start()) + combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: nextPeerId)) + } else { + strongSelf.preloadNextChatPeerIdDisposable.set(nil) + } + } }) } } @@ -3878,6 +3902,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.shareStatusDisposable?.dispose() self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeTarget(self) self.preloadHistoryPeerIdDisposable.dispose() + self.preloadNextChatPeerIdDisposable.dispose() self.reportIrrelvantGeoDisposable?.dispose() self.reminderActivity?.invalidate() self.updateSlowmodeStatusDisposable.dispose() @@ -7053,11 +7078,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let navigationController = strongSelf.effectiveNavigationController { + ApplicationSpecificNotice.incrementNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager).start() + let snapshotState = strongSelf.chatDisplayNode.prepareSnapshotState( titleViewSnapshotState: strongSelf.chatTitleView?.prepareSnapshotState(), avatarSnapshotState: (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() ) - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, completion: { nextController in + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, chatListFilter: strongSelf.currentChatListFilter, completion: { nextController in (nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState) })) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 828771efac..52872c42e7 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -551,6 +551,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var overscrollView: ComponentHostView? var nextChannelToRead: EnginePeer? var offerNextChannelToRead: Bool = false + var nextChannelToReadDisplayName: Bool = false private var currentOverscrollExpandProgress: CGFloat = 0.0 private var feedback: HapticFeedback? var openNextChannelToRead: ((EnginePeer) -> Void)? @@ -1217,15 +1218,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.view.addSubview(overscrollView) } - let expandProgress: CGFloat = min(1.0, max(-offset, 0.0) / 110.0) + let expandDistance = max(-offset - 12.0, 0.0) + let expandProgress: CGFloat = min(1.0, expandDistance / 90.0) let text: String - if self.nextChannelToRead != nil { - if expandProgress >= 0.99 { - //TODO:localize - text = "Release to go to the next unread channel" + if let nextChannelToRead = nextChannelToRead { + if self.nextChannelToReadDisplayName { + if expandProgress >= 0.99 { + //TODO:localize + text = "Release to go to \(nextChannelToRead.compactDisplayTitle)" + } else { + text = "Swipe up to go to \(nextChannelToRead.compactDisplayTitle)" + } } else { - text = "Swipe up to go to the next unread channel" + if expandProgress >= 0.99 { + //TODO:localize + text = "Release to go to the next unread channel" + } else { + text = "Swipe up to go to the next unread channel" + } } let previousType = self.currentOverscrollExpandProgress >= 0.99 @@ -1251,7 +1262,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper), peer: self.nextChannelToRead, context: self.context, - expandProgress: expandProgress + expandDistance: expandDistance )), environment: {}, containerSize: CGSize(width: self.bounds.width, height: 200.0) diff --git a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift index 11ad2f9e29..99ba5e4da8 100644 --- a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift @@ -341,7 +341,7 @@ final class ChatOverscrollControl: CombinedComponent { let foregroundColor: UIColor let peer: EnginePeer? let context: AccountContext - let expandProgress: CGFloat + let expandDistance: CGFloat init( text: String, @@ -349,14 +349,14 @@ final class ChatOverscrollControl: CombinedComponent { foregroundColor: UIColor, peer: EnginePeer?, context: AccountContext, - expandProgress: CGFloat + expandDistance: CGFloat ) { self.text = text self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer self.context = context - self.expandProgress = expandProgress + self.expandDistance = expandDistance } static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool { @@ -375,7 +375,7 @@ final class ChatOverscrollControl: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.expandProgress != rhs.expandProgress { + if lhs.expandDistance != rhs.expandDistance { return false } return true @@ -431,13 +431,32 @@ final class ChatOverscrollControl: CombinedComponent { ) }) + let progressSize = avatarBackground.size.width - avatarProgressPadding * 2.0 + + let halfDistance = progressSize + let quarterDistance = halfDistance / 2.0 + + let clippedDistance = max(0.0, min(halfDistance * 2.0, context.component.expandDistance)) + + var mappedProgress: CGFloat + if clippedDistance <= quarterDistance { + mappedProgress = acos(1.0 - clippedDistance / quarterDistance) / (CGFloat.pi * 2.0) + } else if clippedDistance <= halfDistance { + let sectionDistance = halfDistance - clippedDistance + mappedProgress = 0.25 + asin(1.0 - sectionDistance / quarterDistance) / (CGFloat.pi * 2.0) + } else { + let restDistance = clippedDistance - halfDistance + mappedProgress = min(1.0, 0.5 + restDistance / 60.0) + } + mappedProgress = max(0.01, mappedProgress) + let avatarExpandProgress = avatarExpandProgress.update( component: RadialProgressComponent( color: context.component.foregroundColor, lineWidth: 2.5, - value: context.component.peer == nil ? 0.0 : context.component.expandProgress + value: context.component.peer == nil ? 0.0 : mappedProgress ), - availableSize: CGSize(width: avatarBackground.size.width - avatarProgressPadding * 2.0, height: avatarBackground.size.height - avatarProgressPadding * 2.0), + availableSize: CGSize(width: progressSize, height: progressSize), transition: context.transition ) diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index da44828a2a..d3edf3e890 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -70,7 +70,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam }) } } else { - controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData) + controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData, chatListFilter: params.chatListFilter) } controller.purposefulAction = params.purposefulAction if let search = params.activateMessageSearch {