diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 221da39ac0..8db6591368 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -185,7 +185,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }) + }, present: { _ in }, openForumThread: { _, _ in }) let items = (0 ..< 2).map { _ -> ChatListItem in let message = EngineMessage( diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 76bf442de3..1d12fb5238 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1862,6 +1862,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } }, present: { c in interaction.present(c, nil) + }, openForumThread: { [weak self] peerId, threadId in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + interaction.dismissInput() + interaction.openPeer(peer, peer, threadId, false) + self.listNode.clearHighlightAnimated(true) + }) }) let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in @@ -3056,7 +3069,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }) + }, present: { _ in }, openForumThread: { _, _ in }) let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 3a717ba13a..58f1c5a6c5 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -536,13 +536,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let makeTopicTitleLayout = TextNode.asyncLayout(currentNode?.topicTitleNode) return { constrainedWidth, context, theme, title, iconId, iconColor in - let remainingWidth = max(1.0, constrainedWidth - (22.0 + 2.0)) + let remainingWidth = max(1.0, constrainedWidth - (18.0 + 2.0)) let topicTitleArguments = TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) let topicTitleLayout = makeTopicTitleLayout(topicTitleArguments) - return (CGSize(width: 22.0 + 2.0 + topicTitleLayout.0.size.width, height: topicTitleLayout.0.size.height), { + return (CGSize(width: 18.0 + 2.0 + topicTitleLayout.0.size.width, height: topicTitleLayout.0.size.height), { let topicTitleNode = topicTitleLayout.1() let titleTopicIconView: ComponentHostView @@ -556,7 +556,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let fileId = iconId, fileId != 0 { titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2)) } else { - titleTopicIconContent = .topic(title: String(title.string.prefix(1)), color: iconColor, size: CGSize(width: 22.0, height: 22.0)) + titleTopicIconContent = .topic(title: String(title.string.prefix(1)), color: iconColor, size: CGSize(width: 18.0, height: 18.0)) } let titleTopicIconComponent = EmojiStatusComponent( @@ -576,11 +576,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition: .immediate, component: AnyComponent(titleTopicIconComponent), environment: {}, - containerSize: CGSize(width: 22.0, height: 22.0) + containerSize: CGSize(width: 18.0, height: 18.0) ) - titleTopicIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: UIScreenPixel), size: iconSize) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize) - topicTitleNode.frame = CGRect(origin: CGPoint(x: 22.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size) + topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size) return targetNode }) @@ -592,6 +592,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let authorNode: TextNode var titleTopicArrowNode: ASImageNode? var topicNodes: [Int64: TopicItemNode] = [:] + var topicNodeOrder: [Int64] = [] var visibilityStatus: Bool = false { didSet { @@ -612,6 +613,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.authorNode) } + func setFirstTopicHighlighted(_ isHighlighted: Bool) { + guard let id = self.topicNodeOrder.first, let itemNode = self.topicNodes[id] else { + return + } + if isHighlighted { + itemNode.layer.removeAnimation(forKey: "opacity") + itemNode.alpha = 0.65 + } else { + itemNode.alpha = 1.0 + itemNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + } + } + func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topics: [(id: Int64, title: NSAttributedString, iconId: Int64?, iconColor: Int32)]) -> (CGSize, () -> CGRect?) { let makeAuthorLayout = TextNode.asyncLayout(self.authorNode) var makeExistingTopicLayouts: [Int64: (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32) -> (CGSize, () -> TopicItemNode)] = [:] @@ -649,7 +663,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (topicSize, topicApply) = makeTopicLayout(remainingWidth, context, theme, topic.title, topic.iconId, topic.iconColor) topicsSizeAndApply.append((topic.id, topicSize, topicApply)) - remainingWidth -= topicSize.width + 1.0 + remainingWidth -= topicSize.width + 4.0 } var size = authorTitleLayout.0.size @@ -695,7 +709,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } var topTopicRect: CGRect? + var topicNodeOrder: [Int64] = [] for item in topicsSizeAndApply { + topicNodeOrder.append(item.0) let itemNode = item.2() if self.topicNodes[item.0] != itemNode { self.topicNodes[item.0]?.removeFromSupernode() @@ -707,7 +723,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if topTopicRect == nil { topTopicRect = itemFrame } - nextX += item.1.width + 1.0 + nextX += item.1.width + 4.0 } var removeIds: [Int64] = [] for (id, itemNode) in self.topicNodes { @@ -719,61 +735,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { for id in removeIds { self.topicNodes.removeValue(forKey: id) } - - /*if let topic { - let titleTopicIconView: ComponentHostView - if let current = self.titleTopicIconView { - titleTopicIconView = current - } else { - titleTopicIconView = ComponentHostView() - self.titleTopicIconView = titleTopicIconView - self.view.addSubview(titleTopicIconView) - } - - let titleTopicIconContent: EmojiStatusComponent.Content - if let fileId = topic.iconId, fileId != 0 { - titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2)) - } else { - titleTopicIconContent = .topic(title: String(topic.title.string.prefix(1)), color: topic.iconColor, size: CGSize(width: 22.0, height: 22.0)) - } - - let titleTopicIconComponent = EmojiStatusComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - content: titleTopicIconContent, - isVisibleForAnimations: self.visibilityStatus, - action: nil - ) - self.titleTopicIconComponent = titleTopicIconComponent - - let iconSize = titleTopicIconView.update( - transition: .immediate, - component: AnyComponent(titleTopicIconComponent), - environment: {}, - containerSize: CGSize(width: 22.0, height: 22.0) - ) - titleTopicIconView.frame = CGRect(origin: CGPoint(x: nextX, y: UIScreenPixel), size: iconSize) - nextX += iconSize.width + 2.0 - } else { - if let titleTopicIconView = self.titleTopicIconView { - self.titleTopicIconView = nil - titleTopicIconView.removeFromSuperview() - } - } - - if let topicTitleLayout = topicTitleLayout { - let topicTitleNode = topicTitleLayout.1() - if topicTitleNode.supernode == nil { - self.addSubnode(topicTitleNode) - self.topicTitleNode = topicTitleNode - } - - topicTitleNode.frame = CGRect(origin: CGPoint(x: nextX - 1.0, y: 0.0), size: topicTitleLayout.0.size) - } else if let topicTitleNode = self.topicTitleNode { - self.topicTitleNode = nil - topicTitleNode.removeFromSupernode() - }*/ + self.topicNodeOrder = topicNodeOrder return topTopicRect }) @@ -799,6 +761,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let titleNode: TextNode let authorNode: AuthorNode private var compoundHighlightingNode: LinkHighlightingNode? + private var textArrowNode: ASImageNode? + private var compoundTextButtonNode: HighlightTrackingButtonNode? let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNodeWithEntities @@ -2086,7 +2050,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if !textLeftCutout.isZero { textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + + var textMaxWidth = rawContentWidth - badgeSize + + var textArrowImage: UIImage? + if !forumThreads.isEmpty { + textArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + textMaxWidth -= 18.0 + } + + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let maxTitleLines: Int switch item.index { @@ -2519,31 +2492,101 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size) strongSelf.textNode.textNode.frame = textNodeFrame + if let textArrowImage = textArrowImage { + let textArrowNode: ASImageNode + if let current = strongSelf.textArrowNode { + textArrowNode = current + } else { + textArrowNode = ASImageNode() + strongSelf.textArrowNode = textArrowNode + strongSelf.textNode.textNode.addSubnode(textArrowNode) + } + textArrowNode.image = textArrowImage + let arrowScale: CGFloat = 0.75 + let textArrowSize = CGSize(width: floor(textArrowImage.size.width * arrowScale), height: floor(textArrowImage.size.height * arrowScale)) + textArrowNode.frame = CGRect(origin: CGPoint(x: textNodeFrame.width - 3.0, y: floorToScreenPixels((textNodeFrame.height - textArrowSize.height) / 2.0)), size: textArrowSize) + } else if let textArrowNode = strongSelf.textArrowNode { + strongSelf.textArrowNode = nil + textArrowNode.removeFromSupernode() + } + if let topForumTopicRect { let compoundHighlightingNode: LinkHighlightingNode if let current = strongSelf.compoundHighlightingNode { compoundHighlightingNode = current } else { - compoundHighlightingNode = LinkHighlightingNode(color: theme.itemHighlightedBackgroundColor) + compoundHighlightingNode = LinkHighlightingNode(color: .clear) + compoundHighlightingNode.alpha = strongSelf.authorNode.alpha + compoundHighlightingNode.useModernPathCalculation = true strongSelf.compoundHighlightingNode = compoundHighlightingNode strongSelf.contextContainer.insertSubnode(compoundHighlightingNode, at: 0) } - compoundHighlightingNode.outerRadius = 8.0 - compoundHighlightingNode.innerRadius = 8.0 - compoundHighlightingNode.frame = CGRect(origin: CGPoint(x: authorNodeFrame.minX, y: authorNodeFrame.minY), size: CGSize(width: textNodeFrame.maxX - authorNodeFrame.minX, height: textNodeFrame.maxY - authorNodeFrame.minY)) + + let compoundTextButtonNode: HighlightTrackingButtonNode + if let current = strongSelf.compoundTextButtonNode { + compoundTextButtonNode = current + } else { + compoundTextButtonNode = HighlightTrackingButtonNode() + strongSelf.compoundTextButtonNode = compoundTextButtonNode + strongSelf.contextContainer.addSubnode(compoundTextButtonNode) + compoundTextButtonNode.addTarget(strongSelf, action: #selector(strongSelf.compoundTextButtonPressed), forControlEvents: .touchUpInside) + compoundTextButtonNode.highligthedChanged = { highlighted in + guard let strongSelf = self, let compoundHighlightingNode = strongSelf.compoundHighlightingNode else { + return + } + if highlighted { + compoundHighlightingNode.layer.removeAnimation(forKey: "opacity") + compoundHighlightingNode.alpha = 0.65 + strongSelf.textNode.textNode.alpha = strongSelf.authorNode.alpha * 0.65 + strongSelf.authorNode.setFirstTopicHighlighted(true) + } else { + compoundHighlightingNode.alpha = 1.0 + compoundHighlightingNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + + let prevAlpha = strongSelf.textNode.textNode.alpha + strongSelf.textNode.textNode.alpha = strongSelf.authorNode.alpha + strongSelf.textNode.textNode.layer.animateAlpha(from: prevAlpha, to: strongSelf.authorNode.alpha, duration: 0.2) + strongSelf.authorNode.setFirstTopicHighlighted(false) + } + } + } + + compoundHighlightingNode.color = theme.itemHighlightedBackgroundColor.withMultipliedAlpha(0.5) + var topRect = topForumTopicRect - topRect.origin.y += 1.0 + topRect.origin.x -= 1.0 + topRect.size.width += 2.0 var textRect = textNodeFrame.offsetBy(dx: -authorNodeFrame.minX, dy: -authorNodeFrame.minY) textRect.origin.x = topRect.minX - textRect.size.height -= 3.0 - let midY = floor((topForumTopicRect.minY + textRect.maxY) / 2.0) + 3.0 + textRect.size.height -= 1.0 + textRect.size.width += 16.0 + + compoundHighlightingNode.frame = CGRect(origin: CGPoint(x: authorNodeFrame.minX, y: authorNodeFrame.minY), size: CGSize(width: textNodeFrame.maxX - authorNodeFrame.minX, height: textNodeFrame.maxY - authorNodeFrame.minY)) + + let midY = floor((topForumTopicRect.minY + textRect.maxY) / 2.0) + 1.0 + + let finalTopRect = CGRect(origin: topRect.origin, size: CGSize(width: topRect.width, height: midY - topRect.minY)) + let finalBottomRect = CGRect(origin: CGPoint(x: textRect.minX, y: midY), size: CGSize(width: textRect.width, height: textRect.maxY - midY)) + + compoundHighlightingNode.inset = 0.0 + compoundHighlightingNode.outerRadius = floor(finalBottomRect.height * 0.5) + compoundHighlightingNode.innerRadius = 4.0 + compoundHighlightingNode.updateRects([ - CGRect(origin: topRect.origin, size: CGSize(width: topRect.width, height: midY - topRect.minY)), - CGRect(origin: CGPoint(x: textRect.minX, y: midY), size: CGSize(width: textRect.width, height: textRect.maxY - midY)) + finalTopRect, + finalBottomRect ]) - } else if let compoundHighlightingNode = strongSelf.compoundHighlightingNode { - strongSelf.compoundHighlightingNode = nil - compoundHighlightingNode.removeFromSupernode() + + compoundTextButtonNode.frame = compoundHighlightingNode.frame + } else { + if let compoundHighlightingNode = strongSelf.compoundHighlightingNode { + strongSelf.compoundHighlightingNode = nil + compoundHighlightingNode.removeFromSupernode() + } + if let compoundTextButtonNode = strongSelf.compoundTextButtonNode { + strongSelf.compoundTextButtonNode = nil + compoundTextButtonNode.removeFromSupernode() + } } if !textLayout.spoilers.isEmpty { @@ -2585,12 +2628,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 1.0 strongSelf.textNode.textNode.alpha = 0.0 strongSelf.authorNode.alpha = 0.0 + strongSelf.compoundHighlightingNode?.alpha = 0.0 strongSelf.dustNode?.alpha = 0.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.compoundHighlightingNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) strongSelf.dustNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) } } @@ -2599,6 +2644,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 0.0 strongSelf.textNode.textNode.alpha = 1.0 strongSelf.authorNode.alpha = 1.0 + strongSelf.compoundHighlightingNode?.alpha = 1.0 strongSelf.dustNode?.alpha = 1.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in @@ -2608,6 +2654,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.compoundHighlightingNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.dustNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } else { strongSelf.inputActivitiesNode.removeFromSupernode() @@ -2672,6 +2719,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let authorPosition = strongSelf.authorNode.position transition.animatePosition(node: strongSelf.authorNode, from: CGPoint(x: authorPosition.x - contentDelta.x, y: authorPosition.y - contentDelta.y)) + if let compoundHighlightingNode = strongSelf.compoundHighlightingNode { + let compoundHighlightingPosition = compoundHighlightingNode.position + transition.animatePosition(node: compoundHighlightingNode, from: CGPoint(x: compoundHighlightingPosition.x - contentDelta.x, y: compoundHighlightingPosition.y - contentDelta.y)) + } } if crossfadeContent { @@ -2791,6 +2842,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + @objc private func compoundTextButtonPressed() { + guard let item else { + return + } + guard case let .peer(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, topForumTopicItems) = item.content else { + return + } + guard let topicItem = topForumTopicItems.first else { + return + } + guard case let .chatList(index) = item.index else { + return + } + item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id) + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } @@ -2955,8 +3022,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let titleFrame = self.titleNode.frame transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size)) - let authorFrame = self.authorNode.frame - transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size)) + var authorFrame = self.authorNode.frame + authorFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size) + transition.updateFrame(node: self.authorNode, frame: authorFrame) + + if let compoundHighlightingNode = self.compoundHighlightingNode { + let compoundHighlightingFrame = compoundHighlightingNode.frame + transition.updateFrame(node: compoundHighlightingNode, frame: CGRect(origin: CGPoint(x: authorFrame.minX, y: compoundHighlightingFrame.origin.y), size: compoundHighlightingFrame.size)) + } transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size)) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index e9bd29bbe3..1eec52fa64 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -79,6 +79,7 @@ public final class ChatListNodeInteraction { let hidePsa: (EnginePeer.Id) -> Void let activateChatPreview: (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void let present: (ViewController) -> Void + let openForumThread: (EnginePeer.Id, Int64) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -113,7 +114,8 @@ public final class ChatListNodeInteraction { toggleThreadsSelection: @escaping ([Int64], Bool) -> Void, hidePsa: @escaping (EnginePeer.Id) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, - present: @escaping (ViewController) -> Void + present: @escaping (ViewController) -> Void, + openForumThread: @escaping (EnginePeer.Id, Int64) -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -141,6 +143,7 @@ public final class ChatListNodeInteraction { self.present = present self.animationCache = animationCache self.animationRenderer = animationRenderer + self.openForumThread = openForumThread } } @@ -1141,6 +1144,17 @@ public final class ChatListNode: ListView { } }, present: { [weak self] c in self?.present?(c) + }, openForumThread: { [weak self] peerId, threadId in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.peerSelected?(peer, threadId, true, true, nil) + }) }) let viewProcessingQueue = self.viewProcessingQueue diff --git a/submodules/Display/Source/LinkHighlightingNode.swift b/submodules/Display/Source/LinkHighlightingNode.swift index 2d0440bcf7..a348b4d20f 100644 --- a/submodules/Display/Source/LinkHighlightingNode.swift +++ b/submodules/Display/Source/LinkHighlightingNode.swift @@ -52,7 +52,7 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point: } } -private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { +private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) { if rects.isEmpty { return (CGPoint(), nil) } @@ -77,21 +77,117 @@ private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, context.setBlendMode(.copy) - /*context.move(to: CGPoint(x: rects[0].midX, y: rects[0].minY)) - - for i in 0 ..< rects.count { - let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) - var next: CGRect? - if i + 1 < rects.count { - next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + var rects = rects.map { $0.insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) } + if rects.count > 1 { + let minRadius: CGFloat = 2.0 + + for _ in 0 ..< rects.count * rects.count { + var hadChanges = false + for i in 0 ..< rects.count - 1 { + if rects[i].maxY > rects[i + 1].minY { + let midY = floor((rects[i].maxY + rects[i + 1].minY) * 0.5) + rects[i].size.height = midY - rects[i].minY + rects[i + 1].origin.y = midY + rects[i + 1].size.height = rects[i + 1].maxY - midY + hadChanges = true + } + if rects[i].maxY >= rects[i + 1].minY && rects[i].insetBy(dx: 0.0, dy: 1.0).intersects(rects[i + 1]) { + if abs(rects[i].minX - rects[i + 1].minX) < minRadius { + let commonMinX = min(rects[i].origin.x, rects[i + 1].origin.x) + if rects[i].origin.x != commonMinX { + rects[i].origin.x = commonMinX + hadChanges = true + } + if rects[i + 1].origin.x != commonMinX { + rects[i + 1].origin.x = commonMinX + hadChanges = true + } + } + if abs(rects[i].maxX - rects[i + 1].maxX) < minRadius { + let commonMaxX = max(rects[i].maxX, rects[i + 1].maxX) + if rects[i].maxX != commonMaxX { + rects[i].size.width = commonMaxX - rects[i].minX + hadChanges = true + } + if rects[i + 1].maxX != commonMaxX { + rects[i + 1].size.width = commonMaxX - rects[i + 1].minX + hadChanges = true + } + } + } + } + if !hadChanges { + break + } } - if let next = next { + if useModernPathCalculation { + context.move(to: CGPoint(x: rects[0].midX, y: rects[0].minY)) + context.addLine(to: CGPoint(x: rects[0].maxX - outerRadius, y: rects[0].minY)) + context.addArc(tangent1End: rects[0].topRight, tangent2End: CGPoint(x: rects[0].maxX, y: rects[0].minY + outerRadius), radius: outerRadius) + context.addLine(to: CGPoint(x: rects[0].maxX, y: rects[0].midY)) - } else { + for i in 0 ..< rects.count - 1 { + let rect = rects[i] + let next = rects[i + 1] + + if rect.maxX == next.maxX { + context.addLine(to: CGPoint(x: next.maxX, y: next.midY)) + } else { + let nextRadius = min(outerRadius, floor(abs(rect.maxX - next.maxX) * 0.5)) + context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - nextRadius)) + if next.maxX > rect.maxX { + context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX + nextRadius, y: rect.maxY), radius: nextRadius) + context.addLine(to: CGPoint(x: next.maxX - nextRadius, y: next.minY)) + } else { + context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX - nextRadius, y: rect.maxY), radius: nextRadius) + context.addLine(to: CGPoint(x: next.maxX + nextRadius, y: next.minY)) + } + context.addArc(tangent1End: next.topRight, tangent2End: CGPoint(x: next.maxX, y: next.minY + nextRadius), radius: nextRadius) + context.addLine(to: CGPoint(x: next.maxX, y: next.midY)) + } + } + let last = rects[rects.count - 1] + context.addLine(to: CGPoint(x: last.maxX, y: last.maxY - outerRadius)) + context.addArc(tangent1End: last.bottomRight, tangent2End: CGPoint(x: last.maxX - outerRadius, y: last.maxY), radius: outerRadius) + context.addLine(to: CGPoint(x: last.minX + outerRadius, y: last.maxY)) + context.addArc(tangent1End: last.bottomLeft, tangent2End: CGPoint(x: last.minX, y: last.maxY - outerRadius), radius: outerRadius) + + for i in (1 ..< rects.count).reversed() { + let rect = rects[i] + let prev = rects[i - 1] + + if rect.minX == prev.minX { + context.addLine(to: CGPoint(x: prev.minX, y: prev.midY)) + } else { + let prevRadius = min(outerRadius, floor(abs(rect.minX - prev.minX) * 0.5)) + context.addLine(to: CGPoint(x: rect.minX, y: rect.minY + prevRadius)) + if rect.minX < prev.minX { + context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX + prevRadius, y: rect.minY), radius: prevRadius) + context.addLine(to: CGPoint(x: prev.minX - prevRadius, y: prev.maxY)) + } else { + context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX - prevRadius, y: rect.minY), radius: prevRadius) + context.addLine(to: CGPoint(x: prev.minX + prevRadius, y: prev.maxY)) + } + context.addArc(tangent1End: prev.bottomLeft, tangent2End: CGPoint(x: prev.minX, y: prev.maxY - prevRadius), radius: prevRadius) + context.addLine(to: CGPoint(x: prev.minX, y: prev.midY)) + } + } + + context.addLine(to: CGPoint(x: rects[0].minX, y: rects[0].minY + outerRadius)) + context.addArc(tangent1End: rects[0].topLeft, tangent2End: CGPoint(x: rects[0].minX + outerRadius, y: rects[0].minY), radius: outerRadius) + context.addLine(to: CGPoint(x: rects[0].midX, y: rects[0].minY)) + + context.fillPath() + return } - }*/ + } else { + let path = UIBezierPath(roundedRect: rects[0], cornerRadius: outerRadius).cgPath + context.addPath(path) + context.fillPath() + return + } for i in 0 ..< rects.count { let rect = rects[i].insetBy(dx: -inset, dy: -inset) @@ -181,6 +277,7 @@ public final class LinkHighlightingNode: ASDisplayNode { public var innerRadius: CGFloat = 4.0 public var outerRadius: CGFloat = 4.0 public var inset: CGFloat = 2.0 + public var useModernPathCalculation: Bool = false private var _color: UIColor public var color: UIColor { @@ -218,7 +315,7 @@ public final class LinkHighlightingNode: ASDisplayNode { if self.rects.isEmpty { self.imageNode.image = nil } - let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius) + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, useModernPathCalculation: self.useModernPathCalculation) if let image = image { self.imageNode.image = image @@ -226,11 +323,11 @@ public final class LinkHighlightingNode: ASDisplayNode { } } - public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect]) -> (CGPoint, UIImage)? { + public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect], useModernPathCalculation: Bool) -> (CGPoint, UIImage)? { if rects.isEmpty { return nil } - let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius) + let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation) if let image = image { return (offset, image) @@ -245,11 +342,12 @@ public final class LinkHighlightingNode: ASDisplayNode { let currentInnerRadius = self.innerRadius let currentOuterRadius = self.outerRadius let currentInset = self.inset + let useModernPathCalculation = self.useModernPathCalculation return { [weak self] color, rects, innerRadius, outerRadius, inset in var updatedImage: (CGPoint, UIImage?)? if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset { - updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius) + updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation) } return { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index b0bfae6a9d..d61ede8a25 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -89,6 +89,7 @@ public final class HashtagSearchController: TelegramBaseController { }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in + }, openForumThread: { _, _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 04494158b3..19acc8dd0e 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1273,7 +1273,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, #if DEBUG //debugSaveState(basePath: basePath + "/db", name: "previous2") - debugRestoreState(basePath: basePath + "/db", name: "previous2") + //debugRestoreState(basePath: basePath + "/db", name: "previous2") #endif let startTime = CFAbsoluteTimeGetCurrent() diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 2470b63321..41b41ad488 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }) + }, present: { _ in }, openForumThread: { _, _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 7745b9f833..c88731f7a5 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -843,7 +843,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }) + }, openForumThread: { _, _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) func makeChatListItem( diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index f17b12a0bd..871fd2bab7 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -367,7 +367,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }) + }, openForumThread: { _, _ in }) func makeChatListItem( peer: EnginePeer, diff --git a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index ce3b14a45e..02a7f46301 100644 --- a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -138,11 +138,11 @@ final class HistoryViewStateValidationContexts { } func updateView(id: Int32, view: MessageHistoryView?, location: ChatLocationInput?) { - #if DEBUG + /*#if DEBUG if "".isEmpty { return } - #endif + #endif*/ assert(self.queue.isCurrent()) guard let view = view, view.tagMask == nil || view.tagMask == MessageTags.unseenPersonalMessage || view.tagMask == MessageTags.unseenReaction || view.tagMask == MessageTags.music || view.tagMask == MessageTags.pinned else { diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index b542bbedfe..e099103a24 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -208,7 +208,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { backgroundMaskImage = (currentOffset, currentImage) } else { - backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects) + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) backgroundMaskUpdated = true } diff --git a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift index 828b3a8e8d..3e59b91bdb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift @@ -212,7 +212,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { backgroundMaskImage = (currentOffset, currentImage) } else { - backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects) + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) backgroundMaskUpdated = true } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index dbd4a6324e..6c60274f15 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -259,6 +259,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe gesture?.cancel() } }, present: { _ in + }, openForumThread: { _, _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction