From e1eefc21bb26115727dcfdaca5cabe9e38e569d0 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 23 Apr 2024 00:33:29 +0400 Subject: [PATCH] Next topic overscroll --- .../Peers/TelegramEnginePeers.swift | 26 ++++ .../Chat/ChatOverscrollControl/BUILD | 2 + .../Sources/ChatOverscrollControl.swift | 119 ++++++++++++++++-- .../Chat/ChatControllerLoadDisplayNode.swift | 26 +++- .../TelegramUI/Sources/ChatController.swift | 29 ++++- .../Sources/ChatHistoryListNode.swift | 16 ++- 6 files changed, 199 insertions(+), 19 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 3cfd6892cf..6f1e12f55b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -937,6 +937,32 @@ public extension TelegramEngine { } } } + + public func getNextUnreadForumTopic(peerId: PeerId, topicId: Int32) -> Signal<(id: Int64, data: MessageHistoryThreadData)?, NoError> { + return self.account.postbox.transaction { transaction -> (id: Int64, data: MessageHistoryThreadData)? in + var unreadThreads: [(id: Int64, data: MessageHistoryThreadData, index: MessageIndex)] = [] + for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 100) { + if item.threadId == Int64(topicId) { + continue + } + guard let data = item.info.data.get(MessageHistoryThreadData.self) else { + continue + } + if data.incomingUnreadCount <= 0 { + continue + } + guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else { + continue + } + unreadThreads.append((item.threadId, data, messageIndex)) + } + if let result = unreadThreads.min(by: { $0.index > $1.index }) { + return (result.id, result.data) + } else { + return nil + } + } + } public func getOpaqueChatInterfaceState(peerId: PeerId, threadId: Int64?) -> Signal { return self.account.postbox.transaction { transaction -> OpaqueChatInterfaceState? in diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD index f716408639..6596b91582 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/TextFormat", "//submodules/Markdown", "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index 8bdcac9712..b505081b99 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -9,6 +9,8 @@ import AvatarNode import TextFormat import Markdown import WallpaperBackgroundNode +import EmojiStatusComponent +import TelegramPresentationData final class BlurredRoundedRectangle: Component { let color: UIColor @@ -403,6 +405,16 @@ final class BadgeComponent: CombinedComponent { } } +public struct ChatOverscrollThreadData: Equatable { + public var id: Int64 + public var data: MessageHistoryThreadData + + public init(id: Int64, data: MessageHistoryThreadData) { + self.id = id + self.data = data + } +} + final class AvatarComponent: Component { final class Badge: Equatable { let count: Int @@ -431,6 +443,7 @@ final class AvatarComponent: Component { let context: AccountContext let peer: EnginePeer + let threadData: ChatOverscrollThreadData? let badge: Badge? let rect: CGRect let withinSize: CGSize @@ -439,6 +452,7 @@ final class AvatarComponent: Component { init( context: AccountContext, peer: EnginePeer, + threadData: ChatOverscrollThreadData?, badge: Badge?, rect: CGRect, withinSize: CGSize, @@ -446,6 +460,7 @@ final class AvatarComponent: Component { ) { self.context = context self.peer = peer + self.threadData = threadData self.badge = badge self.rect = rect self.withinSize = withinSize @@ -459,6 +474,9 @@ final class AvatarComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.badge != rhs.badge { return false } @@ -475,17 +493,20 @@ final class AvatarComponent: Component { } final class View: UIView { - private let avatarNode: AvatarNode + private let avatarContainer: UIView + private var avatarNode: AvatarNode? + private var avatarIcon: ComponentView? + private let avatarMask: CAShapeLayer private var badgeView: ComponentHostView? init() { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarContainer = UIView() self.avatarMask = CAShapeLayer() super.init(frame: CGRect()) - self.addSubview(self.avatarNode.view) + self.addSubview(self.avatarContainer) } required init?(coder aDecoder: NSCoder) { @@ -493,9 +514,73 @@ final class AvatarComponent: Component { } func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { - self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) + self.avatarContainer.frame = CGRect(origin: CGPoint(), size: availableSize) let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme - self.avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true) + + if let threadData = component.threadData { + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.view.removeFromSuperview() + } + + let avatarIconContent: EmojiStatusComponent.Content + if threadData.id == 1 { + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) + } else if let fileId = threadData.data.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.data.info.title.prefix(1)), color: threadData.data.info.iconColor, size: CGSize(width: 32.0, height: 32.0)) + } + + let avatarIcon: ComponentView + if let current = self.avatarIcon { + avatarIcon = current + } else { + avatarIcon = ComponentView() + self.avatarIcon = avatarIcon + } + + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: true, + action: nil + ) + + let iconSize = avatarIcon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: availableSize + ) + + let avatarIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + if let avatarIconView = avatarIcon.view { + if avatarIconView.superview == nil { + self.avatarContainer.addSubview(avatarIconView) + } + avatarIconView.frame = avatarIconFrame + } + } else { + if let avatarIcon = self.avatarIcon { + self.avatarIcon = nil + avatarIcon.view?.removeFromSuperview() + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarNode = avatarNode + self.avatarContainer.addSubview(avatarNode.view) + } + + avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) + avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true) + } if let badge = component.badge { let badgeView: ComponentHostView @@ -528,14 +613,14 @@ final class AvatarComponent: Component { ) badgeView.frame = CGRect(origin: CGPoint(x: circlePoint.x - badgeDiameter / 2.0, y: circlePoint.y - badgeDiameter / 2.0), size: badgeSize) - self.avatarMask.frame = self.avatarNode.bounds + self.avatarMask.frame = self.avatarContainer.bounds self.avatarMask.fillRule = .evenOdd let path = UIBezierPath(rect: self.avatarMask.bounds) path.append(UIBezierPath(roundedRect: badgeView.frame.insetBy(dx: -2.0, dy: -2.0), cornerRadius: badgeDiameter / 2.0)) self.avatarMask.path = path.cgPath - self.avatarNode.view.layer.mask = self.avatarMask + self.avatarContainer.layer.mask = self.avatarMask if animateIn { badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14) @@ -547,7 +632,7 @@ final class AvatarComponent: Component { badgeView?.removeFromSuperview() }) - self.avatarNode.view.layer.mask = nil + self.avatarContainer.layer.mask = nil } return availableSize @@ -666,6 +751,7 @@ final class OverscrollContentsComponent: Component { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let expandOffset: CGFloat @@ -679,6 +765,7 @@ final class OverscrollContentsComponent: Component { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, expandOffset: CGFloat, @@ -691,6 +778,7 @@ final class OverscrollContentsComponent: Component { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData self.unreadCount = unreadCount self.location = location self.expandOffset = expandOffset @@ -713,6 +801,9 @@ final class OverscrollContentsComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -870,7 +961,9 @@ final class OverscrollContentsComponent: Component { transition.setSublayerTransform(view: self.avatarScalingContainer.view, transform: CATransform3DMakeScale(avatarExpandProgress, avatarExpandProgress, 1.0)) let titleText: String - if let peer = component.peer { + if let threadData = component.threadData { + titleText = threadData.data.info.title + } else if let peer = component.peer { titleText = peer.compactDisplayTitle } else { titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoChannels @@ -949,6 +1042,7 @@ final class OverscrollContentsComponent: Component { component: AnyComponent(AvatarComponent( context: component.context, peer: peer, + threadData: component.threadData, badge: (isFullyExpanded && component.unreadCount != 0) ? AvatarComponent.Badge(count: component.unreadCount, backgroundColor: component.backgroundColor, foregroundColor: component.foregroundColor) : nil, rect: avatarFrame.offsetBy(dx: self.avatarExtraScalingContainer.frame.midX + component.absoluteRect.minX, dy: self.avatarExtraScalingContainer.frame.midY + component.absoluteRect.minY), withinSize: component.absoluteSize, @@ -988,6 +1082,7 @@ public final class ChatOverscrollControl: CombinedComponent { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let context: AccountContext @@ -1001,6 +1096,7 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, context: AccountContext, @@ -1013,6 +1109,7 @@ public final class ChatOverscrollControl: CombinedComponent { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData self.unreadCount = unreadCount self.location = location self.context = context @@ -1033,6 +1130,9 @@ public final class ChatOverscrollControl: CombinedComponent { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -1070,6 +1170,7 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: context.component.backgroundColor, foregroundColor: context.component.foregroundColor, peer: context.component.peer, + threadData: context.component.threadData, unreadCount: context.component.unreadCount, location: context.component.location, expandOffset: context.component.expandDistance, diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index b67487e4f8..9ace80eb8d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4629,7 +4629,7 @@ extension ChatControllerImpl { downPressed: buttonAction ) - self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer, location in + self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer, threadData, location in guard let strongSelf = self else { return } @@ -4652,12 +4652,32 @@ extension ChatControllerImpl { } var updatedChatNavigationStack = strongSelf.chatNavigationStack - updatedChatNavigationStack.removeAll(where: { $0 == ChatNavigationStackItem(peerId: peer.id, threadId: nil) }) + updatedChatNavigationStack.removeAll(where: { $0 == ChatNavigationStackItem(peerId: peer.id, threadId: threadData?.id) }) if let peerId = strongSelf.chatLocation.peerId { updatedChatNavigationStack.insert(ChatNavigationStackItem(peerId: peerId, threadId: strongSelf.chatLocation.threadId), at: 0) } + + let chatLocation: NavigateToChatControllerParams.Location + if let threadData { + chatLocation = .replyThread(ChatReplyThreadMessage( + peerId: peer.id, + threadId: threadData.id, + channelMessageId: nil, + isChannelPost: false, + isForumPost: true, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )) + } else { + chatLocation = .peer(peer) + } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), animated: false, chatListFilter: nextFolderId, chatNavigationStack: updatedChatNavigationStack, completion: { nextController in + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: chatLocation, animated: false, chatListFilter: nextFolderId, chatNavigationStack: updatedChatNavigationStack, completion: { nextController in (nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState) }, customChatNavigationStack: strongSelf.customChatNavigationStack)) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8e96205b39..d8f1f5d5d0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5707,8 +5707,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer, unreadCount: 0, location: .same) + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: nextPeer, threadData: nil, unreadCount: 0, location: .same) } strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 @@ -5745,8 +5745,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer.peer, unreadCount: nextPeer.unreadCount, location: nextPeer.location) + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location) } strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 @@ -6310,6 +6310,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return interfaceState } }) + + if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil { + strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), + strongSelf.context.engine.peers.getNextUnreadForumTopic(peerId: channel.id, topicId: Int32(clamping: replyThreadId)), + ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) + ) + |> then(.complete() |> delay(1.0, queue: .mainQueue())) + |> restart).startStrict(next: { nextThreadData, nextChatSuggestionTip in + guard let strongSelf = self else { + return + } + + strongSelf.offerNextChannelToRead = true + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextThreadData.flatMap { nextThreadData -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: EnginePeer(channel), threadData: nextThreadData, unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same) + } + strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + + strongSelf.updateNextChannelToReadVisibility() + }) + } } if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 66cac8ff0e..379bbc8026 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -662,14 +662,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var isSelectionGestureEnabled = true private var overscrollView: ComponentHostView? - var nextChannelToRead: (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? + var nextChannelToRead: (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? var offerNextChannelToRead: Bool = false var nextChannelToReadDisplayName: Bool = false private var currentOverscrollExpandProgress: CGFloat = 0.0 private var freezeOverscrollControl: Bool = false private var freezeOverscrollControlProgress: Bool = false private var feedback: HapticFeedback? - var openNextChannelToRead: ((EnginePeer, TelegramEngine.NextUnreadChannelLocation) -> Void)? + var openNextChannelToRead: ((EnginePeer, (id: Int64, data: MessageHistoryThreadData)?, TelegramEngine.NextUnreadChannelLocation) -> Void)? private var contentInsetAnimator: DisplayLinkAnimator? let adMessagesContext: AdMessagesHistoryContext? @@ -1018,7 +1018,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if strongSelf.offerNextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 { if let nextChannelToRead = strongSelf.nextChannelToRead { strongSelf.freezeOverscrollControl = true - strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.location) + strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.threadData, nextChannelToRead.location) } else { strongSelf.freezeOverscrollControlProgress = true strongSelf.scroller.contentInset = UIEdgeInsets(top: 94.0 + 12.0, left: 0.0, bottom: 0.0, right: 0.0) @@ -2235,8 +2235,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto switch nextChannelToRead.location { case .same: if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil { + //TODO:localize swipeText = ("Pull up to go to the next channel", []) releaseText = ("Release to go to the next channel", []) + } else if nextChannelToRead.threadData != nil { + swipeText = ("Pull up to go to the next topic", []) + releaseText = ("Release to go to the next topic", []) } else { swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, []) releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, []) @@ -2280,6 +2284,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper), peer: self.nextChannelToRead?.peer, + threadData: (self.nextChannelToRead?.threadData).flatMap { threadData in + return ChatOverscrollThreadData( + id: threadData.id, + data: threadData.data + ) + }, unreadCount: self.nextChannelToRead?.unreadCount ?? 0, location: self.nextChannelToRead?.location ?? .same, context: self.context,