diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index acd26de1ae..71b378f3b3 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -204,6 +204,7 @@ private enum ChatListSearchItemHeaderId: Hashable { public final class ChatListSearchItemHeader: ListViewItemHeader { public let id: ListViewItemNode.HeaderId + public let stackingId: ListViewItemNode.HeaderId? = nil public let type: ChatListSearchItemHeaderType public let stickDirection: ListViewItemHeaderStickDirection = .top public let stickOverInsets: Bool = true diff --git a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift index fcfe07cd01..9fd3a49dcf 100644 --- a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift +++ b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift @@ -6,6 +6,7 @@ import ListSectionHeaderNode final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { let id: ListViewItemNode.HeaderId + let stackingId: ListViewItemNode.HeaderId? = nil let theme: PresentationTheme let letter: unichar let stickDirection: ListViewItemHeaderStickDirection = .top diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 872fd3622a..5cae045448 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -9,6 +9,11 @@ private let insertionAnimationDuration: Double = 0.4 private struct VisibleHeaderNodeId: Hashable { var id: ListViewItemNode.HeaderId var affinity: Int + + init(id: ListViewItemNode.HeaderId, affinity: Int) { + self.id = id + self.affinity = affinity + } } private final class ListViewBackingLayer: CALayer { @@ -3871,23 +3876,84 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel func addHeader(id: VisibleHeaderNodeId, upperBound: CGFloat, upperIndex: Int, upperBoundEdge: CGFloat, lowerBound: CGFloat, lowerIndex: Int, item: ListViewItemHeader, hasValidNodes: Bool) { let itemHeaderHeight: CGFloat = item.height - let headerFrame: CGRect - let stickLocationDistanceFactor: CGFloat - let stickLocationDistance: CGFloat + var insertItemBelowOtherHeaders = false + var offsetByHeaderNodeId: ListViewItemNode.HeaderId? + var didOffsetByHeaderNode = false + + var headerFrame: CGRect + let naturalY: CGFloat + var stickLocationDistanceFactor: CGFloat = 0.0 + var stickLocationDistance: CGFloat switch item.stickDirection { case .top: + naturalY = lowerBound headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) stickLocationDistance = headerFrame.minY - upperBound stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) case .topEdge: + naturalY = lowerBound headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBoundEdge - itemHeaderHeight), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) stickLocationDistance = headerFrame.minY - upperBoundEdge + itemHeaderHeight stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) case .bottom: + naturalY = lowerBound headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) stickLocationDistance = lowerBound - headerFrame.maxY stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + + if let stackingId = item.stackingId { + insertItemBelowOtherHeaders = true + + var naturalOverlapLowerBound: CGFloat = naturalY + do { + for (otherId, otherNode) in self.itemHeaderNodes { + if otherId.id.space == stackingId.space { + if !visibleHeaderNodes.contains(otherId) { + continue + } + if let otherNaturalOriginY = otherNode.naturalOriginY, otherNaturalOriginY == naturalY { + naturalOverlapLowerBound = otherNaturalOriginY - 7.0 - 20.0 + break + } + } + } + } + + for _ in 0 ..< 2 { + var mostOverlap: (CGRect, CGFloat, ListViewItemHeaderNode)? + for (otherId, otherNode) in self.itemHeaderNodes { + if otherId.id.space == stackingId.space { + if !visibleHeaderNodes.contains(otherId) { + continue + } + if headerFrame.intersects(otherNode.frame) { + let intersectionHeight = headerFrame.intersection(otherNode.frame).height + if intersectionHeight > 0.0 { + if let (currentOverlapFrame, _, _) = mostOverlap { + if headerFrame.minY < currentOverlapFrame.minY { + mostOverlap = (otherNode.frame, intersectionHeight, otherNode) + } + } else { + mostOverlap = (otherNode.frame, intersectionHeight, otherNode) + } + } + } + } + } + if let (mostOverlap, _, otherNode) = mostOverlap { + let originalY = headerFrame.origin.y + headerFrame.origin.y = min(headerFrame.origin.y, mostOverlap.minY - 7.0 - 20.0) + headerFrame.origin.y = max(upperBound, headerFrame.origin.y) + offsetByHeaderNodeId = otherNode.item?.id + didOffsetByHeaderNode = originalY != headerFrame.origin.y + } + } + + stickLocationDistance = naturalOverlapLowerBound - headerFrame.maxY + stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + } } + visibleHeaderNodes.append(id) let initialHeaderNodeAlpha = self.itemHeaderNodesAlpha @@ -3896,7 +3962,14 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel headerNode = current switch transition.0 { case .immediate: + let previousFrame = headerNode.frame headerNode.updateFrame(headerFrame, within: self.visibleSize) + if headerNode.offsetByHeaderNodeId != nil && offsetByHeaderNodeId != nil && headerNode.offsetByHeaderNodeId != offsetByHeaderNodeId { + let _ = didOffsetByHeaderNode + if !previousFrame.isEmpty { + ContainedViewLayoutTransition.animated(duration: 0.35, curve: .spring).animatePositionAdditive(node: headerNode, offset: CGPoint(x: 0.0, y: previousFrame.minY - headerFrame.minY)) + } + } case let .animated(duration, curve): let previousFrame = headerNode.frame headerNode.updateFrame(headerFrame, within: self.visibleSize) @@ -3927,7 +4000,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel item.updateNode(headerNode, previous: nil, next: nil) headerNode.item = item } - headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition.0) + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: animateInsertion ? .immediate : transition.0) headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: true) headerNode.internalStickLocationDistance = stickLocationDistance if !hasValidNodes && !headerNode.alpha.isZero { @@ -3940,7 +4013,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel headerNode.animateAdded(duration: 0.2) } } - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: stickLocationDistance, transition: transition.0) } else { headerNode = item.node(synchronousLoad: synchronousLoad) headerNode.alpha = initialHeaderNodeAlpha @@ -3950,10 +4023,28 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } headerNode.updateFlashingOnScrolling(flashing, animated: false) headerNode.frame = headerFrame - headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition.0) + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: animateInsertion ? .immediate : transition.0) headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: false) self.itemHeaderNodes[id] = headerNode - if let verticalScrollIndicator = self.verticalScrollIndicator { + if insertItemBelowOtherHeaders { + var lowestHeaderNode: ASDisplayNode? + var lowestHeaderNodeIndex: Int? + for (_, headerNode) in self.itemHeaderNodes { + if let index = self.view.subviews.firstIndex(of: headerNode.view) { + if lowestHeaderNodeIndex == nil || index < lowestHeaderNodeIndex! { + lowestHeaderNodeIndex = index + lowestHeaderNode = headerNode + } + } + } + if let lowestHeaderNode { + self.insertSubnode(headerNode, belowSubnode: lowestHeaderNode) + } else if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(headerNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(headerNode) + } + } else if let verticalScrollIndicator = self.verticalScrollIndicator { self.insertSubnode(headerNode, belowSubnode: verticalScrollIndicator) } else { self.addSubnode(headerNode) @@ -3962,8 +4053,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel headerNode.alpha = initialHeaderNodeAlpha headerNode.animateAdded(duration: 0.2) } - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: stickLocationDistance, transition: .immediate) } + headerNode.offsetByHeaderNodeId = offsetByHeaderNodeId + headerNode.naturalOriginY = naturalY var maxIntersectionHeight: (CGFloat, Int)? for i in upperIndex ... lowerIndex { let itemNode = self.itemNodes[i] @@ -4008,53 +4101,65 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel var previousHeaderBySpace: [AnyHashable: (id: VisibleHeaderNodeId, upperBound: CGFloat, upperBoundIndex: Int, upperBoundEdge: CGFloat, lowerBound: CGFloat, lowerBoundIndex: Int, item: ListViewItemHeader, hasValidNodes: Bool)] = [:] - for i in 0 ..< self.itemNodes.count { - let itemNode = self.itemNodes[i] - let itemFrame = itemNode.apparentFrame - let itemTopInset = itemNode.insets.top - var validItemHeaderSpaces: [AnyHashable] = [] - if let itemHeaders = itemNode.headers() { - for itemHeader in itemHeaders { - guard let affinity = itemNode.headerSpaceAffinities[itemHeader.id] else { - assertionFailure() - continue - } - - let headerId = VisibleHeaderNodeId(id: itemHeader.id, affinity: affinity) - - validItemHeaderSpaces.append(itemHeader.id.space) - - let itemMaxY: CGFloat - if itemHeader.stickOverInsets { - itemMaxY = itemFrame.maxY - } else { - itemMaxY = itemFrame.maxY - (self.rotated ? itemNode.insets.top : itemNode.insets.bottom) - } - - if let (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, previousLowerBound, previousLowerIndex, previousHeaderItem, hasValidNodes) = previousHeaderBySpace[itemHeader.id.space] { - if previousHeaderId == headerId { - previousHeaderBySpace[itemHeader.id.space] = (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, itemMaxY, i, previousHeaderItem, hasValidNodes || itemNode.index != nil) + for phase in 0 ..< 2 { + for i in 0 ..< self.itemNodes.count { + let itemNode = self.itemNodes[i] + let itemFrame = itemNode.apparentFrame + let itemTopInset = itemNode.insets.top + var validItemHeaderSpaces: [AnyHashable] = [] + if let itemHeaders = itemNode.headers() { + outerItemHeaders: for itemHeader in itemHeaders { + if phase == 0 { + if itemHeader.stackingId != nil { + continue outerItemHeaders + } + } else { + if itemHeader.stackingId == nil { + continue outerItemHeaders + } + } + + guard let affinity = itemNode.headerSpaceAffinities[itemHeader.id] else { + assertionFailure() + continue + } + + let headerId = VisibleHeaderNodeId(id: itemHeader.id, affinity: affinity) + + validItemHeaderSpaces.append(itemHeader.id.space) + + var itemMaxY: CGFloat + if itemHeader.stickOverInsets { + itemMaxY = itemFrame.maxY + } else { + itemMaxY = itemFrame.maxY - (self.rotated ? itemNode.insets.top : itemNode.insets.bottom) + } + + if let (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, previousLowerBound, previousLowerIndex, previousHeaderItem, hasValidNodes) = previousHeaderBySpace[itemHeader.id.space] { + if previousHeaderId == headerId { + previousHeaderBySpace[itemHeader.id.space] = (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, itemMaxY, i, previousHeaderItem, hasValidNodes || itemNode.index != nil) + } else { + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperIndex: previousUpperIndex, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, lowerIndex: previousLowerIndex, item: previousHeaderItem, hasValidNodes: hasValidNodes) + + previousHeaderBySpace[itemHeader.id.space] = (headerId, itemFrame.minY, i, itemFrame.minY + itemTopInset, itemMaxY, i, itemHeader, itemNode.index != nil) + } } else { - addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperIndex: previousUpperIndex, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, lowerIndex: previousLowerIndex, item: previousHeaderItem, hasValidNodes: hasValidNodes) - previousHeaderBySpace[itemHeader.id.space] = (headerId, itemFrame.minY, i, itemFrame.minY + itemTopInset, itemMaxY, i, itemHeader, itemNode.index != nil) } - } else { - previousHeaderBySpace[itemHeader.id.space] = (headerId, itemFrame.minY, i, itemFrame.minY + itemTopInset, itemMaxY, i, itemHeader, itemNode.index != nil) } } - } - - for (space, previousHeader) in previousHeaderBySpace { - if validItemHeaderSpaces.contains(space) { - continue + + for (space, previousHeader) in previousHeaderBySpace { + if validItemHeaderSpaces.contains(space) { + continue + } + + let (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, previousLowerBound, previousLowerIndex, previousHeaderItem, hasValidNodes) = previousHeader + + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperIndex: previousUpperIndex, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, lowerIndex: previousLowerIndex, item: previousHeaderItem, hasValidNodes: hasValidNodes) + + previousHeaderBySpace.removeValue(forKey: space) } - - let (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, previousLowerBound, previousLowerIndex, previousHeaderItem, hasValidNodes) = previousHeader - - addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperIndex: previousUpperIndex, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, lowerIndex: previousLowerIndex, item: previousHeaderItem, hasValidNodes: hasValidNodes) - - previousHeaderBySpace.removeValue(forKey: space) } } diff --git a/submodules/Display/Source/ListViewItemHeader.swift b/submodules/Display/Source/ListViewItemHeader.swift index 286dade8cf..98a43c73b0 100644 --- a/submodules/Display/Source/ListViewItemHeader.swift +++ b/submodules/Display/Source/ListViewItemHeader.swift @@ -10,6 +10,7 @@ public enum ListViewItemHeaderStickDirection { public protocol ListViewItemHeader: AnyObject { var id: ListViewItemNode.HeaderId { get } + var stackingId: ListViewItemNode.HeaderId? { get } var stickDirection: ListViewItemHeaderStickDirection { get } var height: CGFloat { get } var stickOverInsets: Bool { get } @@ -29,6 +30,9 @@ open class ListViewItemHeaderNode: ASDisplayNode { private var isFlashingOnScrolling = false weak var attachedToItemNode: ListViewItemNode? + var offsetByHeaderNodeId: ListViewItemNode.HeaderId? + var naturalOriginY: CGFloat? + public var item: ListViewItemHeader? func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { @@ -61,7 +65,7 @@ open class ListViewItemHeaderNode: ASDisplayNode { self.isLayerBacked = layerBacked } - open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + open func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) { } final func addScrollingOffset(_ scrollingOffset: CGFloat) { diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 44cd222b2d..499100d72b 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -2045,6 +2045,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo public final class ItemListPeerItemHeader: ListViewItemHeader { public let id: ListViewItemNode.HeaderId + public let stackingId: ListViewItemNode.HeaderId? = nil public let context: AccountContext public let text: NSAttributedString public let additionalText: String @@ -2229,7 +2230,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true) } - override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) { if self.stickDistanceFactor == factor { return } diff --git a/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift b/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift index 6c27a3fae4..40cd573723 100644 --- a/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift +++ b/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift @@ -42,6 +42,7 @@ final class ListMessageDateHeader: ListViewItemHeader { private let year: Int32 let id: ListViewItemNode.HeaderId + let stackingId: ListViewItemNode.HeaderId? = nil let theme: PresentationTheme let strings: PresentationStrings let fontSize: PresentationFontSize diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 507d313574..e9f0486efa 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -2776,6 +2776,10 @@ final class MessageHistoryTable: Table { associatedThreadInfo = self.seedConfiguration.decodeMessageThreadInfo(data.data) } + if let threadId = message.threadId, let possibleThreadPeer = peerTable.get(PeerId(threadId)) { + peers[possibleThreadPeer.id] = possibleThreadPeer + } + return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, threadId: message.threadId, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: message.customTags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds, associatedMedia: associatedMedia, associatedThreadInfo: associatedThreadInfo, associatedStories: associatedStories) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 8758b43e1c..360e066bee 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -498,6 +498,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } } + switch message { case let .message(_, attributes, _, _, threadId, replyToMessageId, _, _, _, _): if let replyToMessageId = replyToMessageId, (replyToMessageId.messageId.peerId != peerId && peerId.namespace == Namespaces.Peer.SecretChat), let replyMessage = transaction.getMessage(replyToMessageId.messageId) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 298fc080ab..0841a546a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -583,9 +583,6 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } } } else if let inputChannel = maybePeer.flatMap(apiInputChannel) { - if let channel = maybePeer as? TelegramChannel, channel.flags.contains(.isMonoforum) { - return .single(false) - } let fullChannelSignal = network.request(Api.functions.channels.getFullChannel(channel: inputChannel)) |> map(Optional.init) |> `catch` { error -> Signal in @@ -594,10 +591,17 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } return .single(nil) } - let participantSignal = network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) - |> map(Optional.init) - |> `catch` { error -> Signal in - return .single(nil) + + + let participantSignal: Signal + if let channel = maybePeer as? TelegramChannel, channel.flags.contains(.isMonoforum) { + participantSignal = .single(nil) + } else { + participantSignal = network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) + |> map(Optional.init) + |> `catch` { error -> Signal in + return .single(nil) + } } return combineLatest(fullChannelSignal, participantSignal) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index da52aa556f..289c647211 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -801,7 +801,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var displaySize = CGSize(width: 180.0, height: 180.0) let telegramFile = self.telegramFile let emojiFile = self.emojiFile @@ -823,7 +823,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) - func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) @@ -1005,8 +1005,15 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight + if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight + } else { + if dateHeaderAtBottom.hasDate { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + if dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } } var deliveryFailedInset: CGFloat = 0.0 @@ -1377,6 +1384,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.updateAccessibilityData(accessibilityData) + strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic) + strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -1829,7 +1838,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let weakSelf = Weak(self) - return { (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) in + return { (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) in return continueAsyncLayout(weakSelf, item, params, mergedTop, mergedBottom, dateHeaderAtBottom) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 14b8433d01..ac63032d20 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -728,7 +728,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var forceStopAnimations: Bool = false - typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: Bool) + typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: ChatMessageHeaderSpec) private var currentInputParams: Params? private var currentApplyParams: ListViewItemApply? @@ -1394,7 +1394,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } - override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { if let message = contentNode.item?.message { @@ -1431,7 +1431,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) - return ChatMessageBubbleItemNode.beginLayout(selfReference: weakSelf, item, params, mergedTop, mergedBottom, dateHeaderAtBottom, + return ChatMessageBubbleItemNode.beginLayout( + selfReference: weakSelf, + item: item, + params: params, + mergedTop: mergedTop, + mergedBottom: mergedBottom, + dateHeaderAtBottom: dateHeaderAtBottom, currentContentClassesPropertiesAndLayouts: currentContentClassesPropertiesAndLayouts, authorNameLayout: authorNameLayout, viaMeasureLayout: viaMeasureLayout, @@ -1456,11 +1462,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private static func beginLayout( selfReference: Weak, - _ item: ChatMessageItem, - _ params: ListViewItemLayoutParams, - _ mergedTop: ChatMessageMerge, - _ mergedBottom: ChatMessageMerge, - _ dateHeaderAtBottom: Bool, + item: ChatMessageItem, + params: ListViewItemLayoutParams, + mergedTop: ChatMessageMerge, + mergedBottom: ChatMessageMerge, + dateHeaderAtBottom: ChatMessageHeaderSpec, currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), @@ -2620,7 +2626,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var hasThreadInfo = false - if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1 || item.associatedData.isRecentActions), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.isForumOrMonoForum, item.message.associatedThreadInfo != nil { + if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1 || item.associatedData.isRecentActions), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.isForum, item.message.associatedThreadInfo != nil { hasThreadInfo = true } else if case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { @@ -3219,11 +3225,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight - } - if isAd { - layoutInsets.top += 4.0 + if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight + } else { + if dateHeaderAtBottom.hasDate { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + if dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + if isAd { + layoutInsets.top += 4.0 + } } let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) @@ -3468,6 +3481,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.updateAttachedAvatarNodeOffset(offset: avatarOffset, transition: .animated(duration: 0.3, curve: .spring)) } strongSelf.updateAttachedAvatarNodeIsHidden(isHidden: isSidePanelOpen, transition: animation.transition) + strongSelf.updateAttachedDateHeader(hasDate: inputParams.dateHeaderAtBottom.hasDate, hasPeer: inputParams.dateHeaderAtBottom.hasTopic) let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp()) if isFailed { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 2bbaf56794..4a4932ecf2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -277,7 +277,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco } } - override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let layoutConstants = self.layoutConstants let makeVideoLayout = self.interactiveVideoNode.asyncLayout() @@ -296,7 +296,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco let currentForwardInfo = self.appliedForwardInfo let currentPlaying = self.appliedCurrentlyPlaying - func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) @@ -391,8 +391,15 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco } var layoutInsets = layoutConstants.instantVideo.insets - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight + if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight + } else { + if dateHeaderAtBottom.hasDate { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + if dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } } var deliveryFailedInset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index e31b95a366..f736e8cbee 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -104,6 +104,20 @@ public enum ChatMessageMerge: Int32 { } } +public struct ChatMessageHeaderSpec: Equatable { + public var hasDate: Bool + public var hasTopic: Bool + + public init(hasDate: Bool, hasTopic: Bool) { + self.hasDate = hasDate + self.hasTopic = hasTopic + } +} + +public protocol ChatMessageDateHeaderNode: ListViewItemHeaderNode { + func updateItem(hasDate: Bool, hasPeer: Bool) +} + public protocol ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { func updateSelectionState(animated: Bool) func updateAvatarIsHidden(isHidden: Bool, transition: ContainedViewLayoutTransition) @@ -128,7 +142,7 @@ public protocol ChatMessageItem: ListViewItem { var sending: Bool { get } var failed: Bool { get } - func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) + func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: ChatMessageHeaderSpec) } public func hasCommentButton(item: ChatMessageItem) -> Bool { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index f749c3c46a..1b79cf3821 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -108,6 +108,7 @@ public struct ChatMessageItemWallpaperLayoutConstants { public struct ChatMessageItemLayoutConstants { public var avatarDiameter: CGFloat public var timestampHeaderHeight: CGFloat + public var timestampDateAndTopicHeaderHeight: CGFloat public var bubble: ChatMessageItemBubbleLayoutConstants public var image: ChatMessageItemImageLayoutConstants @@ -117,9 +118,10 @@ public struct ChatMessageItemLayoutConstants { public var instantVideo: ChatMessageItemInstantVideoConstants public var wallpapers: ChatMessageItemWallpaperLayoutConstants - public init(avatarDiameter: CGFloat, timestampHeaderHeight: CGFloat, bubble: ChatMessageItemBubbleLayoutConstants, image: ChatMessageItemImageLayoutConstants, video: ChatMessageItemVideoLayoutConstants, text: ChatMessageItemTextLayoutConstants, file: ChatMessageItemFileLayoutConstants, instantVideo: ChatMessageItemInstantVideoConstants, wallpapers: ChatMessageItemWallpaperLayoutConstants) { + public init(avatarDiameter: CGFloat, timestampHeaderHeight: CGFloat, timestampDateAndTopicHeaderHeight: CGFloat, bubble: ChatMessageItemBubbleLayoutConstants, image: ChatMessageItemImageLayoutConstants, video: ChatMessageItemVideoLayoutConstants, text: ChatMessageItemTextLayoutConstants, file: ChatMessageItemFileLayoutConstants, instantVideo: ChatMessageItemInstantVideoConstants, wallpapers: ChatMessageItemWallpaperLayoutConstants) { self.avatarDiameter = avatarDiameter self.timestampHeaderHeight = timestampHeaderHeight + self.timestampDateAndTopicHeaderHeight = timestampDateAndTopicHeaderHeight self.bubble = bubble self.image = image self.video = video @@ -142,7 +144,7 @@ public struct ChatMessageItemLayoutConstants { let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, timestampDateAndTopicHeaderHeight: 7.0 * 2.0 + 20.0 * 2.0 + 7.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) } public static var regular: ChatMessageItemLayoutConstants { @@ -154,7 +156,7 @@ public struct ChatMessageItemLayoutConstants { let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0)) let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, timestampDateAndTopicHeaderHeight: 7.0 * 2.0 + 20.0 * 2.0 + 7.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 17f2c596b1..290c6dcfa4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -16,6 +16,7 @@ import WallpaperBackgroundNode import ChatControllerInteraction import AvatarVideoNode import ChatMessageItem +import AvatarNode private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -28,25 +29,55 @@ private let timezoneOffset: Int32 = { private let granularity: Int32 = 60 * 60 * 24 public final class ChatMessageDateHeader: ListViewItemHeader { + public struct Id: Hashable { + public let roundedTimestamp: Int64? + public let separableThreadId: Int64? + + public init(roundedTimestamp: Int64?, separableThreadId: Int64?) { + self.roundedTimestamp = roundedTimestamp + self.separableThreadId = separableThreadId + } + } + + public final class PeerData { + public let peer: EnginePeer + + public init(peer: EnginePeer) { + self.peer = peer + } + } + private let timestamp: Int32 private let roundedTimestamp: Int32 private let scheduled: Bool + public let displayPeer: PeerData? public let id: ListViewItemNode.HeaderId + public let stackingId: ListViewItemNode.HeaderId? + public let idValue: Id public let presentationData: ChatPresentationData public let controllerInteraction: ChatControllerInteraction? public let context: AccountContext public let action: ((Int32, Bool) -> Void)? - public init(timestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { + public init(timestamp: Int32, separableThreadId: Int64?, scheduled: Bool, displayPeer: PeerData?, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { self.timestamp = timestamp self.scheduled = scheduled + self.displayPeer = displayPeer self.presentationData = presentationData self.controllerInteraction = controllerInteraction self.context = context self.action = action self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) - self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) + if let _ = self.displayPeer { + self.idValue = ChatMessageDateHeader.Id(roundedTimestamp: 0, separableThreadId: separableThreadId) + self.id = ListViewItemNode.HeaderId(space: 3, id: self.idValue) + self.stackingId = ListViewItemNode.HeaderId(space: 2, id: ChatMessageDateHeader.Id(roundedTimestamp: Int64(self.roundedTimestamp), separableThreadId: nil)) + } else { + self.idValue = ChatMessageDateHeader.Id(roundedTimestamp: Int64(self.roundedTimestamp), separableThreadId: nil) + self.id = ListViewItemNode.HeaderId(space: 2, id: self.idValue) + self.stackingId = nil + } let isRotated = controllerInteraction?.chatIsRotated ?? true @@ -67,11 +98,11 @@ public final class ChatMessageDateHeader: ListViewItemHeader { } public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { - return ChatMessageDateHeaderNode(localTimestamp: self.roundedTimestamp, scheduled: self.scheduled, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction, context: self.context, action: self.action) + return ChatMessageDateHeaderNodeImpl(localTimestamp: self.roundedTimestamp, scheduled: self.scheduled, displayPeer: self.displayPeer, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction, context: self.context, action: self.action) } public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { - guard let node = node as? ChatMessageDateHeaderNode, let next = next as? ChatMessageDateHeader else { + guard let node = node as? ChatMessageDateHeaderNodeImpl, let next = next as? ChatMessageDateHeader else { return } node.updatePresentationData(next.presentationData, context: next.context) @@ -119,32 +150,71 @@ private func dateHeaderTimestampId(timestamp: Int32) -> Int32 { } } -public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { +private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { + private let controllerInteraction: ChatControllerInteraction? + private let presentationData: ChatPresentationData + + private let backgroundNode: NavigationBackgroundNode + private let patternLayer: SimpleShapeLayer + + init( + controllerInteraction: ChatControllerInteraction?, + presentationData: ChatPresentationData + ) { + self.controllerInteraction = controllerInteraction + self.presentationData = presentationData + + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.backgroundNode.isUserInteractionEnabled = false + + self.patternLayer = SimpleShapeLayer() + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.backgroundNode) + + let fullTranslucency: Bool = self.controllerInteraction?.enableFullTranslucency ?? true + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: self.presentationData.theme.theme, wallpaper: self.presentationData.theme.wallpaper), enableBlur: fullTranslucency && dateFillNeedsBlur(theme: self.presentationData.theme.theme, wallpaper: self.presentationData.theme.wallpaper), transition: .immediate) + + self.patternLayer.lineWidth = 1.66 + self.patternLayer.strokeColor = UIColor.white.cgColor + + let linePath = CGMutablePath() + linePath.move(to: CGPoint(x: 0.0, y: self.patternLayer.lineWidth * 0.5)) + linePath.addLine(to: CGPoint(x: 10000.0, y: self.patternLayer.lineWidth * 0.5)) + self.patternLayer.path = linePath + self.patternLayer.lineDashPattern = [6.0 as NSNumber, 2.0 as NSNumber] as [NSNumber] + + self.backgroundNode.layer.mask = self.patternLayer + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 10.0)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: backgroundFrame.size, transition: transition) + + transition.updateFrame(layer: self.patternLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 1.66))) + } +} + +private final class ChatMessageDateContentNode: ASDisplayNode { + private var presentationData: ChatPresentationData + private let controllerInteraction: ChatControllerInteraction? + public let labelNode: TextNode public let backgroundNode: NavigationBackgroundNode public let stickBackgroundNode: ASImageNode private var backgroundContent: WallpaperBubbleBackgroundNode? + private var text: String - private let localTimestamp: Int32 - private var presentationData: ChatPresentationData - private let controllerInteraction: ChatControllerInteraction? - private let context: AccountContext - private let text: String - - private var flashingOnScrolling = false - private var stickDistanceFactor: CGFloat = 0.0 - private var action: ((Int32, Bool) -> Void)? = nil - - private var absolutePosition: (CGRect, CGSize)? - - public init(localTimestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { + init(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, localTimestamp: Int32, scheduled: Bool) { self.presentationData = presentationData self.controllerInteraction = controllerInteraction - self.context = context - - self.localTimestamp = localTimestamp - self.action = action self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false @@ -195,13 +265,7 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } self.text = text - let isRotated = controllerInteraction?.chatIsRotated ?? true - - super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) - - if isRotated { - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - } + super.init() let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) @@ -210,7 +274,7 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: fullTranslucency && dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) self.stickBackgroundNode.image = graphics.dateFloatingBackground self.stickBackgroundNode.alpha = 0.0 - + if let backgroundContent = self.backgroundContent { self.addSubnode(backgroundContent) } else { @@ -227,14 +291,8 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let _ = apply() self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) } - - override public func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - public func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { let previousPresentationData = self.presentationData self.presentationData = presentationData @@ -256,32 +314,21 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { if presentationData.fontSize != previousPresentationData.fontSize { self.labelNode.bounds = CGRect(origin: CGPoint(), size: size.size) } - - self.setNeedsLayout() } - public func updateBackgroundColor(color: UIColor, enableBlur: Bool) { + func updateBackgroundColor(color: UIColor, enableBlur: Bool) { self.backgroundNode.updateColor(color: color, enableBlur: enableBlur, transition: .immediate) } - override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - self.absolutePosition = (rect, containerSize) - if let backgroundContent = self.backgroundContent { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += containerSize.height - rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) - } - } - - override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { let chatDateSize: CGFloat = 20.0 let chatDateInset: CGFloat = 6.0 let labelSize = self.labelNode.bounds.size let backgroundSize = CGSize(width: labelSize.width + chatDateInset * 2.0, height: chatDateSize) - let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: (34.0 - chatDateSize) / 2.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + floorToScreenPixels((size.width - leftInset - rightInset - backgroundSize.width) / 2.0), y: (size.height - chatDateSize) / 2.0), size: backgroundSize) + transition.updateFrame(node: self.stickBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: transition) @@ -297,18 +344,313 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { transition.updateFrame(node: backgroundContent, frame: self.backgroundNode.frame) backgroundContent.cornerRadius = backgroundFrame.size.height / 2.0 - if let (rect, containerSize) = self.absolutePosition { + /*if let (rect, containerSize) = self.absolutePosition { var backgroundFrame = backgroundContent.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += containerSize.height - rect.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) - } + }*/ } } - override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + self.stickBackgroundNode.alpha = factor + } +} + +private final class ChatMessagePeerContentNode: ASDisplayNode { + private let context: AccountContext + private var presentationData: ChatPresentationData + private let controllerInteraction: ChatControllerInteraction? + private let peer: EnginePeer + + private let avatarNode: AvatarNode + public let labelNode: TextNode + public let backgroundNode: NavigationBackgroundNode + public let stickBackgroundNode: ASImageNode + + private var backgroundContent: WallpaperBubbleBackgroundNode? + private var text: String + + init(context: AccountContext, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, peer: EnginePeer) { + self.context = context + self.presentationData = presentationData + self.controllerInteraction = controllerInteraction + self.peer = peer + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + self.labelNode.displaysAsynchronously = !presentationData.isPreview + + if controllerInteraction?.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true, let backgroundContent = controllerInteraction?.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + self.backgroundContent = backgroundContent + } + + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.backgroundNode.isUserInteractionEnabled = false + + self.stickBackgroundNode = ASImageNode() + self.stickBackgroundNode.isLayerBacked = true + self.stickBackgroundNode.displayWithoutProcessing = true + self.stickBackgroundNode.displaysAsynchronously = false + + let text = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + self.text = text + + super.init() + + let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + + let fullTranslucency: Bool = controllerInteraction?.enableFullTranslucency ?? true + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: fullTranslucency && dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) + self.stickBackgroundNode.image = graphics.dateFloatingBackground + self.stickBackgroundNode.alpha = 0.0 + + if let backgroundContent = self.backgroundContent { + self.addSubnode(backgroundContent) + } else { + self.addSubnode(self.backgroundNode) + } + self.addSubnode(self.avatarNode) + self.addSubnode(self.labelNode) + + let titleFont = Font.medium(min(18.0, floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))) + + let attributedString = NSAttributedString(string: text, font: titleFont, textColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) + let labelLayout = TextNode.asyncLayout(self.labelNode) + + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let _ = apply() + self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) + } + + func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + let previousPresentationData = self.presentationData + self.presentationData = presentationData + + let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + + let fullTranslucency: Bool = self.controllerInteraction?.enableFullTranslucency ?? true + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: fullTranslucency && dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) + self.stickBackgroundNode.image = graphics.dateFloatingBackground + + let titleFont = Font.medium(min(18.0, floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))) + + let attributedString = NSAttributedString(string: self.text, font: titleFont, textColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) + let labelLayout = TextNode.asyncLayout(self.labelNode) + + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let _ = apply() + + if presentationData.fontSize != previousPresentationData.fontSize { + self.labelNode.bounds = CGRect(origin: CGPoint(), size: size.size) + } + } + + func updateBackgroundColor(color: UIColor, enableBlur: Bool) { + self.backgroundNode.updateColor(color: color, enableBlur: enableBlur, transition: .immediate) + } + + func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + let chatDateSize: CGFloat = 20.0 + let chatDateInset: CGFloat = 6.0 + + let avatarDiameter: CGFloat = 16.0 + let avatarInset: CGFloat = 2.0 + let avatarSpacing: CGFloat = 4.0 + + let labelSize = self.labelNode.bounds.size + let backgroundSize = CGSize(width: avatarInset + avatarDiameter + avatarSpacing + labelSize.width + chatDateInset, height: chatDateSize) + + let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + floorToScreenPixels((size.width - leftInset - rightInset - backgroundSize.width) / 2.0), y: (size.height - chatDateSize) / 2.0), size: backgroundSize) + + let avatarFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + avatarInset, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + self.avatarNode.updateSize(size: avatarFrame.size) + + if self.peer.smallProfileImage != nil { + self.avatarNode.setPeerV2(context: self.context, theme: self.presentationData.theme.theme, peer: self.peer, displayDimensions: avatarFrame.size) + } else { + self.avatarNode.setPeer(context: self.context, theme: self.presentationData.theme.theme, peer: self.peer, displayDimensions: avatarFrame.size) + } + + transition.updateFrame(node: self.stickBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: transition) + let labelFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + avatarInset + avatarDiameter + avatarSpacing, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - labelSize.height) / 2.0)), size: labelSize) + + transition.updatePosition(node: self.labelNode, position: labelFrame.center) + self.labelNode.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) + + if let backgroundContent = self.backgroundContent { + backgroundContent.allowsGroupOpacity = true + self.backgroundNode.isHidden = true + + transition.updateFrame(node: backgroundContent, frame: self.backgroundNode.frame) + backgroundContent.cornerRadius = backgroundFrame.size.height / 2.0 + + /*if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) + }*/ + } + } + + func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + self.stickBackgroundNode.alpha = factor + } +} + +public final class ChatMessageDateHeaderNodeImpl: ListViewItemHeaderNode, ChatMessageDateHeaderNode { + private var dateContentNode: ChatMessageDateContentNode? + private var peerContentNode: ChatMessagePeerContentNode? + + private var sectionSeparator: ChatMessageDateSectionSeparatorNode? + + private let context: AccountContext + private let localTimestamp: Int32 + private let scheduled: Bool + private let displayPeer: ChatMessageDateHeader.PeerData? + private var presentationData: ChatPresentationData + private let controllerInteraction: ChatControllerInteraction? + + private var flashingOnScrolling = false + private var stickDistanceFactor: CGFloat = 0.0 + private var action: ((Int32, Bool) -> Void)? = nil + + private var params: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? + private var absolutePosition: (CGRect, CGSize)? + + public init(localTimestamp: Int32, scheduled: Bool, displayPeer: ChatMessageDateHeader.PeerData?, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { + self.context = context + self.presentationData = presentationData + self.controllerInteraction = controllerInteraction + + self.localTimestamp = localTimestamp + self.scheduled = scheduled + self.displayPeer = displayPeer + self.action = action + + let isRotated = controllerInteraction?.chatIsRotated ?? true + + super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) + + if isRotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } + + if let displayPeer { + if self.peerContentNode == nil { + let sectionSeparator = ChatMessageDateSectionSeparatorNode(controllerInteraction: controllerInteraction, presentationData: presentationData) + self.sectionSeparator = sectionSeparator + self.addSubnode(sectionSeparator) + + let peerContentNode = ChatMessagePeerContentNode(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction, peer: displayPeer.peer) + self.peerContentNode = peerContentNode + self.addSubnode(peerContentNode) + } + } else { + if self.dateContentNode == nil { + let dateContentNode = ChatMessageDateContentNode(presentationData: self.presentationData, controllerInteraction: self.controllerInteraction, localTimestamp: self.localTimestamp, scheduled: self.scheduled) + self.dateContentNode = dateContentNode + self.addSubnode(dateContentNode) + } + } + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + public func updateItem(hasDate: Bool, hasPeer: Bool) { + } + + public func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + if let dateContentNode = self.dateContentNode { + dateContentNode.updatePresentationData(presentationData, context: context) + } + if let peerContentNode = self.peerContentNode { + peerContentNode.updatePresentationData(presentationData, context: context) + } + + self.setNeedsLayout() + } + + public func updateBackgroundColor(color: UIColor, enableBlur: Bool) { + if let dateContentNode = self.dateContentNode { + dateContentNode.updateBackgroundColor(color: color, enableBlur: enableBlur) + } + if let peerContentNode = self.peerContentNode { + peerContentNode.updateBackgroundColor(color: color, enableBlur: enableBlur) + } + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + /*self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + }*/ + } + + override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.params = (size, leftInset, rightInset) + + var contentOffsetY: CGFloat = 0.0 + var isFirst = true + if let dateContentNode = self.dateContentNode { + if isFirst { + isFirst = false + contentOffsetY += 7.0 + } else { + contentOffsetY += 7.0 + } + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentOffsetY), size: CGSize(width: size.width, height: 20.0)) + transition.updateFrame(node: dateContentNode, frame: contentFrame) + dateContentNode.update(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + contentOffsetY += 20.0 + } + if let peerContentNode = self.peerContentNode { + if isFirst { + isFirst = false + contentOffsetY += 7.0 + } else { + contentOffsetY += 7.0 + } + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentOffsetY), size: CGSize(width: size.width, height: 20.0)) + transition.updateFrame(node: peerContentNode, frame: contentFrame) + peerContentNode.update(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + contentOffsetY += 20.0 + + if let sectionSeparator = self.sectionSeparator { + let sectionSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + floorToScreenPixels((contentFrame.height - 1.66) * 0.5)), size: CGSize(width: size.width, height: 1.66)) + sectionSeparator.update(size: sectionSeparatorFrame.size, transition: transition) + transition.updatePosition(node: sectionSeparator, position: sectionSeparatorFrame.center) + transition.updateBounds(node: sectionSeparator, bounds: CGRect(origin: CGPoint(), size: sectionSeparatorFrame.size)) + } + } + contentOffsetY += 7.0 + } + + override public func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) { if !self.stickDistanceFactor.isEqual(to: factor) { - self.stickBackgroundNode.alpha = factor + if let dateContentNode = self.dateContentNode { + dateContentNode.updateStickDistanceFactor(factor, transition: transition) + } + if let peerContentNode = self.peerContentNode { + peerContentNode.updateStickDistanceFactor(factor, transition: transition) + } let wasZero = self.stickDistanceFactor < 0.5 let isZero = factor < 0.5 @@ -322,6 +664,10 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.updateFlashing(animated: animated) } } + + if let sectionSeparator = self.sectionSeparator { + transition.updateTransform(node: sectionSeparator, transform: CGAffineTransformMakeTranslation(0.0, -distance)) + } } override public func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { @@ -333,34 +679,82 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let flashing = self.flashingOnScrolling || self.stickDistanceFactor < 0.5 let alpha: CGFloat = flashing ? 1.0 : 0.0 - let previousAlpha = self.backgroundNode.alpha - if !previousAlpha.isEqual(to: alpha) { - self.backgroundContent?.alpha = alpha - self.backgroundNode.alpha = alpha - self.labelNode.alpha = alpha - if animated { - let duration: Double = flashing ? 0.3 : 0.4 - self.backgroundContent?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) - self.backgroundNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) - self.labelNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + if let dateContentNode = self.dateContentNode { + let previousAlpha = dateContentNode.alpha + + if !previousAlpha.isEqual(to: alpha) { + dateContentNode.alpha = alpha + if animated { + let duration: Double = flashing ? 0.3 : 0.4 + dateContentNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + } + } + } + + if let peerContentNode = self.peerContentNode { + let previousAlpha = peerContentNode.alpha + + if !previousAlpha.isEqual(to: alpha) { + peerContentNode.alpha = alpha + if animated { + let duration: Double = flashing ? 0.3 : 0.4 + peerContentNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + } } } } + override public func animateRemoved(duration: Double) { + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + if self.dateContentNode != nil { + self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false) + } + } + + override public func animateAdded(duration: Double) { + self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2) + if self.dateContentNode != nil { + self.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } + } + override public func getEffectiveAlpha() -> CGFloat { - return self.backgroundNode.alpha + if let dateContentNode = self.dateContentNode { + return dateContentNode.alpha + } + if let peerContentNode = self.peerContentNode { + return peerContentNode.alpha + } + return 0.0 } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } - if self.labelNode.alpha.isZero { - return nil + if let dateContentNode = self.dateContentNode { + if dateContentNode.frame.contains(point) { + if dateContentNode.alpha.isZero { + return nil + } + + if dateContentNode.backgroundNode.frame.contains(point.offsetBy(dx: -dateContentNode.frame.minX, dy: -dateContentNode.frame.minY)) { + return self.view + } + } } - if self.backgroundNode.frame.contains(point) { - return self.view + if let peerContentNode = self.peerContentNode { + if peerContentNode.frame.contains(point) { + if peerContentNode.alpha.isZero { + return nil + } + + if peerContentNode.backgroundNode.frame.contains(point.offsetBy(dx: -peerContentNode.frame.minX, dy: -peerContentNode.frame.minY)) { + return self.view + } + } } return nil } @@ -383,6 +777,7 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader { } public let id: ListViewItemNode.HeaderId + public let stackingId: ListViewItemNode.HeaderId? = nil public let peerId: PeerId public let peer: Peer? public let messageReference: MessageReference? @@ -740,7 +1135,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat self.avatarNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) } - override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) { } override public func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 9f553a7c55..e201f39da8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -220,6 +220,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible public let additionalContent: ChatMessageItemAdditionalContent? let dateHeader: ChatMessageDateHeader + let topicHeader: ChatMessageDateHeader? let avatarHeader: ChatMessageAvatarHeader? public let headers: [ListViewItemHeader] @@ -286,6 +287,8 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible var displayAuthorInfo: Bool let messagePeerId: PeerId = chatLocation.peerId ?? content.firstMessage.id.peerId + var headerSeparableThreadId: Int64? + var headerDisplayPeer: ChatMessageDateHeader.PeerData? do { let peerId = messagePeerId @@ -320,6 +323,12 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let channel = content.firstMessage.peers[content.firstMessage.id.peerId] as? TelegramChannel, channel.isMonoForum { if case .replyThread = chatLocation { displayAuthorInfo = false + } else { + headerSeparableThreadId = content.firstMessage.threadId + + if let threadId = content.firstMessage.threadId, let peer = content.firstMessage.peers[EnginePeer.Id(threadId)] { + headerDisplayPeer = ChatMessageDateHeader.PeerData(peer: EnginePeer(peer)) + } } } } @@ -332,7 +341,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible isScheduledMessages = true } - self.dateHeader = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: isScheduledMessages, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context, action: { timestamp, alreadyThere in + self.dateHeader = ChatMessageDateHeader(timestamp: content.index.timestamp, separableThreadId: nil, scheduled: isScheduledMessages, displayPeer: nil, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context, action: { timestamp, alreadyThere in var calendar = NSCalendar.current calendar.timeZone = TimeZone(abbreviation: "UTC")! let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) @@ -343,6 +352,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } }) + if let headerSeparableThreadId, let headerDisplayPeer { + self.topicHeader = ChatMessageDateHeader(timestamp: content.index.timestamp, separableThreadId: headerSeparableThreadId, scheduled: false, displayPeer: headerDisplayPeer, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context, action: { _, _ in + controllerInteraction.updateChatLocationThread(headerSeparableThreadId) + }) + } else { + self.topicHeader = nil + } + if displayAuthorInfo { let message = content.firstMessage var hasActionMedia = false @@ -395,6 +412,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible var headers: [ListViewItemHeader] = [] if !self.disableDate { headers.append(self.dateHeader) + if let topicHeader = self.topicHeader { + headers.append(topicHeader) + } } if case .messageOptions = associatedData.subject { headers = [] @@ -499,7 +519,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) + let (layout, apply) = nodeLayout(self, params, top, bottom, disableDate ? ChatMessageHeaderSpec(hasDate: false, hasTopic: false) : dateAtBottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -526,7 +546,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: ChatMessageHeaderSpec) { var top = top var bottom = bottom if !isRotated { @@ -537,7 +557,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none - var dateAtBottom = false + var dateAtBottom = ChatMessageHeaderSpec(hasDate: false, hasTopic: false) if let top = top as? ChatMessageItemImpl { if top.dateHeader.id != self.dateHeader.id { mergedBottom = .none @@ -548,20 +568,35 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let bottom = bottom as? ChatMessageItemImpl { if bottom.dateHeader.id != self.dateHeader.id { mergedTop = .none - dateAtBottom = true - } else { + dateAtBottom.hasDate = true + } + if let topicHeader = self.topicHeader, bottom.topicHeader?.id != topicHeader.id { + mergedTop = .none + dateAtBottom.hasTopic = true + } + + if !(dateAtBottom.hasDate || dateAtBottom.hasTopic) { mergedTop = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, bottom.message, message) } } else if let bottom = bottom as? ChatUnreadItem { if bottom.header.id != self.dateHeader.id { - dateAtBottom = true + dateAtBottom.hasDate = true + } + if self.topicHeader != nil { + dateAtBottom.hasTopic = true } } else if let bottom = bottom as? ChatReplyCountItem { if bottom.header.id != self.dateHeader.id { - dateAtBottom = true + dateAtBottom.hasDate = true + } + if self.topicHeader != nil { + dateAtBottom.hasTopic = true } } else { - dateAtBottom = true + dateAtBottom.hasDate = true + if self.topicHeader != nil { + dateAtBottom.hasTopic = true + } } return (mergedTop, mergedBottom, dateAtBottom) @@ -589,7 +624,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) + let (layout, apply) = nodeLayout(self, params, top, bottom, disableDate ? ChatMessageHeaderSpec(hasDate: false, hasTopic: false) : dateAtBottom) Queue.mainQueue().async { completion(layout, { info in apply(animation, info, false) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift index aa9b14c971..3a193abfdd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift @@ -25,7 +25,7 @@ public class ChatReplyCountItem: ListViewItem { self.isComments = isComments self.count = count self.presentationData = presentationData - self.header = ChatMessageDateHeader(timestamp: index.timestamp, scheduled: false, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context) + self.header = ChatMessageDateHeader(timestamp: index.timestamp, separableThreadId: nil, scheduled: false, displayPeer: nil, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context) self.controllerInteraction = controllerInteraction } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift index 28e53f9b3a..0865ae6cfc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift @@ -22,7 +22,7 @@ public class ChatUnreadItem: ListViewItem { self.index = index self.presentationData = presentationData self.controllerInteraction = controllerInteraction - self.header = ChatMessageDateHeader(timestamp: index.timestamp, scheduled: false, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context) + self.header = ChatMessageDateHeader(timestamp: index.timestamp, separableThreadId: nil, scheduled: false, displayPeer: nil, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context) } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 3875398d0a..b8bcac5dac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -716,7 +716,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { } } - open func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + open func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { return { _, _, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _, _, _ in @@ -902,6 +902,8 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { private var attachedAvatarNodeOffset: CGFloat = 0.0 private var attachedAvatarNodeIsHidden: Bool = false + + private var attachedDateHeader: (hasDate: Bool, hasPeer: Bool) = (false, false) override open func attachedHeaderNodesUpdated() { if !self.attachedAvatarNodeOffset.isZero { @@ -919,6 +921,12 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { headerNode.updateAvatarIsHidden(isHidden: self.attachedAvatarNodeIsHidden, transition: .immediate) } } + + for headerNode in self.attachedHeaderNodes { + if let headerNode = headerNode as? ChatMessageDateHeaderNode { + headerNode.updateItem(hasDate: self.attachedDateHeader.hasDate, hasPeer: self.attachedDateHeader.hasPeer) + } + } } open func updateAttachedAvatarNodeOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -939,6 +947,15 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { } } + open func updateAttachedDateHeader(hasDate: Bool, hasPeer: Bool) { + self.attachedDateHeader = (hasDate, hasPeer) + for headerNode in self.attachedHeaderNodes { + if let headerNode = headerNode as? ChatMessageDateHeaderNode { + headerNode.updateItem(hasDate: hasDate, hasPeer: hasPeer) + } + } + } + open func unreadMessageRangeUpdated() { } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index a2228c18aa..21d4c80c12 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -417,7 +417,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } - override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let displaySize = CGSize(width: 184.0, height: 184.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -435,7 +435,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { let currentShareButtonNode = self.shareButtonNode let currentForwardInfo = self.appliedForwardInfo - func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + func continueAsyncLayout(_ weakSelf: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) @@ -567,8 +567,15 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight + if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight + } else { + if dateHeaderAtBottom.hasDate { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + if dateHeaderAtBottom.hasTopic { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } } var deliveryFailedInset: CGFloat = 0.0 @@ -964,6 +971,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.updateAccessibilityData(accessibilityData) + strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic) + transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame) strongSelf.enableSynchronousImageApply = true imageApply() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 1e0dd952d7..0fc2b9719f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -734,7 +734,21 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextColor: messageTheme.primaryTextColor, spoilerEffectColor: messageTheme.secondaryTextColor, areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji, - spoilerExpandRect: spoilerExpandRect + spoilerExpandRect: spoilerExpandRect, + crossfadeContents: { [weak strongSelf] sourceView in + guard let strongSelf else { + return + } + if let textNodeContainer = strongSelf.textNode.textNode.view.superview { + sourceView.frame = CGRect(origin: strongSelf.textNode.textNode.frame.origin, size: sourceView.bounds.size) + textNodeContainer.addSubview(sourceView) + + sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak sourceView] _ in + sourceView?.removeFromSuperview() + }) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + } ) )) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 8f8aa84c8b..0e05b95c73 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -239,6 +239,7 @@ public final class ChatSideTopicsPanel: Component { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) + avatarNode.isUserInteractionEnabled = false self.avatarNode = avatarNode self.containerButton.addSubview(avatarNode.view) } @@ -597,6 +598,18 @@ public final class ChatSideTopicsPanel: Component { } } + public func topicIndex(threadId: Int64?) -> Int? { + if let threadId { + if let value = self.items.firstIndex(where: { $0.id == .chatList(PeerId(threadId)) }) { + return value + 1 + } else { + return nil + } + } else { + return 0 + } + } + func update(component: ChatSideTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index db6bb21216..e11c583606 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -1099,19 +1099,22 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn public let spoilerEffectColor: UIColor public let areContentAnimationsEnabled: Bool public let spoilerExpandRect: CGRect? + public var crossfadeContents: ((UIView) -> Void)? public init( animation: ListViewItemUpdateAnimation, spoilerTextColor: UIColor, spoilerEffectColor: UIColor, areContentAnimationsEnabled: Bool, - spoilerExpandRect: CGRect? + spoilerExpandRect: CGRect?, + crossfadeContents: ((UIView) -> Void)? = nil ) { self.animation = animation self.spoilerTextColor = spoilerTextColor self.spoilerEffectColor = spoilerEffectColor self.areContentAnimationsEnabled = areContentAnimationsEnabled self.spoilerExpandRect = spoilerExpandRect + self.crossfadeContents = crossfadeContents } } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift index 1949e4f8c6..4d4c9e26c8 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift @@ -216,9 +216,14 @@ public final class InteractiveTextNodeWithEntities { return (layout, { applyArguments in let animation: ListViewItemUpdateAnimation = applyArguments.applyArguments.animation + var crossfadeSourceView: UIView? + if let maybeNode, applyArguments.applyArguments.animation.transition.isAnimated, let animator = applyArguments.applyArguments.animation.animator as? ControlledTransition.LegacyAnimator, animator.transition.isAnimated, maybeNode.textNode.bounds.size != layout.size { + crossfadeSourceView = maybeNode.textNode.view.snapshotView(afterScreenUpdates: false) + } + let result = apply(applyArguments.applyArguments) - if let maybeNode = maybeNode { + if let maybeNode { maybeNode.attributedString = arguments.attributedString maybeNode.updateInteractiveContents( @@ -234,6 +239,10 @@ public final class InteractiveTextNodeWithEntities { applyArguments: applyArguments.applyArguments ) + if let crossfadeSourceView { + applyArguments.applyArguments.crossfadeContents?(crossfadeSourceView) + } + return maybeNode } else { let resultNode = InteractiveTextNodeWithEntities(textNode: result) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 8bbc388aaa..f59ec62296 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -338,7 +338,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { headerNode.item = header } headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate) } else { headerNode = header.node(synchronousLoad: true) if headerNode.item !== header { @@ -350,7 +350,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.itemHeaderNodes[id] = headerNode strongSelf.containerNode.addSubnode(headerNode) - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 117648521c..b65e392f6a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -403,10 +403,14 @@ extension ChatControllerImpl { } else { messageOptionsTitleInfo = .single(nil) } - + + var isFirstTimeValue = true self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo) |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in if let strongSelf = self { + let isFirstTime = isFirstTimeValue + isFirstTimeValue = false + var isScheduledMessages = false if case .scheduledMessages = presentationInterfaceState.subject { isScheduledMessages = true @@ -446,6 +450,8 @@ extension ChatControllerImpl { } else if let peer = peerViewMainPeer(peerView) { if case .pinnedMessages = presentationInterfaceState.subject { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) + } else if let channel = peer as? TelegramChannel, channel.isMonoForum { + strongSelf.chatTitleView?.titleContent = .custom(channel.debugDisplayTitle, nil, false) } else { strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) let imageOverride: AvatarNodeImageOverride? @@ -460,7 +466,7 @@ extension ChatControllerImpl { } else { imageOverride = nil } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride, synchronousLoad: isFirstTime) if case .standard(.previewing) = strongSelf.mode { (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false } else { @@ -767,16 +773,22 @@ extension ChatControllerImpl { autoremoveTimeout = value?.effectiveValue } } else if let cachedChannelData = peerView.cachedData as? CachedChannelData { - currentSendAsPeerId = cachedChannelData.sendAsPeerId - if let channel = peer as? TelegramChannel, case .group = channel.info { - if !cachedChannelData.botInfos.isEmpty { - hasBots = true + if let channel = peer as? TelegramChannel, channel.isMonoForum { + if case let .known(value) = cachedChannelData.linkedMonoforumPeerId { + currentSendAsPeerId = value } - let botCommands = cachedChannelData.botInfos.reduce(into: [], { result, info in - result.append(contentsOf: info.botInfo.commands) - }) - if !botCommands.isEmpty { - hasBotCommands = true + } else { + currentSendAsPeerId = cachedChannelData.sendAsPeerId + if let channel = peer as? TelegramChannel, case .group = channel.info { + if !cachedChannelData.botInfos.isEmpty { + hasBots = true + } + let botCommands = cachedChannelData.botInfos.reduce(into: [], { result, info in + result.append(contentsOf: info.botInfo.commands) + }) + if !botCommands.isEmpty { + hasBotCommands = true + } } } if case let .known(value) = cachedChannelData.autoremoveTimeout { @@ -1139,22 +1151,39 @@ extension ChatControllerImpl { savedMessagesPeerId = nil } - let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int)?, NoError> + let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int, presence: EnginePeer.Presence?)?, NoError> if let savedMessagesPeerId { let threadPeerId = savedMessagesPeerId - let basicPeerKey: PostboxViewKey = .basicPeer(threadPeerId) + let basicPeerKey: PostboxViewKey = .peer(peerId: threadPeerId, components: []) let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: savedMessagesPeerId.toInt64(), namespace: Namespaces.Message.Cloud, customTag: nil) savedMessagesPeer = context.account.postbox.combinedView(keys: [basicPeerKey, countViewKey]) - |> map { views -> (peer: EnginePeer?, messageCount: Int)? in - let peer = ((views.views[basicPeerKey] as? BasicPeerView)?.peer).flatMap(EnginePeer.init) + |> map { views -> (peer: EnginePeer?, messageCount: Int, presence: EnginePeer.Presence?)? in + var peer: EnginePeer? + var presence: EnginePeer.Presence? + if let peerView = views.views[basicPeerKey] as? PeerView { + peer = peerViewMainPeer(peerView).flatMap(EnginePeer.init) + presence = peerView.peerPresences[threadPeerId].flatMap(EnginePeer.Presence.init) + } var messageCount = 0 if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { messageCount += Int(count) } - return (peer, messageCount) + return (peer, messageCount, presence) } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs?.peer != rhs?.peer { + return false + } + if lhs?.messageCount != rhs?.messageCount { + return false + } + if lhs?.presence != rhs?.presence { + return false + } + return true + }) } else { savedMessagesPeer = .single(nil) } @@ -1261,6 +1290,7 @@ extension ChatControllerImpl { let globalPrivacySettings = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()) self.titleDisposable.set(nil) + var isFirstTimeValue = true self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView, messageAndTopic, @@ -1275,6 +1305,9 @@ extension ChatControllerImpl { ) |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, globalPrivacySettings in if let strongSelf = self { + let isFirstTime = isFirstTimeValue + isFirstTimeValue = false + strongSelf.hasScheduledMessages = hasScheduledMessages var renderedPeer: RenderedPeer? @@ -1334,16 +1367,29 @@ extension ChatControllerImpl { } if let savedMessagesPeerId { + var peerPresences: [PeerId: PeerPresence] = [:] + if let presence = savedMessagesPeer?.presence { + peerPresences[savedMessagesPeerId] = presence._asPresence() + } let mappedPeerData = ChatTitleContent.PeerData( peerId: savedMessagesPeerId, peer: savedMessagesPeer?.peer?._asPeer(), isContact: true, isSavedMessages: true, notificationSettings: nil, - peerPresences: [:], + peerPresences: peerPresences, cachedData: nil ) - strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) + + var customMessageCount: Int? + if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.isMonoForum { + } else { + customMessageCount = savedMessagesPeer?.messageCount ?? 0 + } + + strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) + + if selfController.rightNavigationButton != button { strongSelf.peerView = peerView @@ -1374,7 +1420,7 @@ extension ChatControllerImpl { .updatedHasScheduledMessages(hasScheduledMessages) }) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer, overrideImage: imageOverride) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer, overrideImage: imageOverride, synchronousLoad: isFirstTime) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile } else { @@ -1464,12 +1510,18 @@ extension ChatControllerImpl { var peerGeoLocation: PeerGeoLocation? var currentSendAsPeerId: PeerId? if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { - currentSendAsPeerId = cachedData.sendAsPeerId - if case .group = peer.info { - peerGeoLocation = cachedData.peerGeoLocation - } - if case let .known(value) = cachedData.linkedDiscussionPeerId { - peerDiscussionId = value + if peer.isMonoForum { + if case let .known(value) = cachedData.linkedMonoforumPeerId { + currentSendAsPeerId = value + } + } else { + currentSendAsPeerId = cachedData.sendAsPeerId + if case .group = peer.info { + peerGeoLocation = cachedData.peerGeoLocation + } + if case let .known(value) = cachedData.linkedDiscussionPeerId { + peerDiscussionId = value + } } } @@ -1685,11 +1737,15 @@ extension ChatControllerImpl { self.chatTitleView?.titleContent = .custom(" ", nil, false) } + var isFirstTimeValue = true self.peerDisposable.set((peerView |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in guard let self else { return } + + let isFirstTime = isFirstTimeValue + isFirstTimeValue = false var renderedPeer: RenderedPeer? if let peerView, let peer = peerView.peers[peerView.peerId] { @@ -1700,7 +1756,7 @@ extension ChatControllerImpl { } renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) - (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: self.context, theme: self.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil) + (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: self.context, theme: self.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil, synchronousLoad: isFirstTime) } self.peerView = peerView @@ -2327,6 +2383,10 @@ extension ChatControllerImpl { return } + if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.isMonoForum { + return + } + let isPremium = strongSelf.presentationInterfaceState.isPremium var allPeers: [SendAsPeer]? diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 7e02a681e8..01338e9f8c 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -23,6 +23,8 @@ func updateChatPresentationInterfaceStateImpl( _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void ) { + let previousChatLocation = selfController.chatDisplayNode.historyNode.chatLocation + var completion = externalCompletion var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState) @@ -434,6 +436,11 @@ func updateChatPresentationInterfaceStateImpl( selfController.presentationInterfaceState = updatedChatPresentationInterfaceState + if selfController.chatDisplayNode.chatLocation != selfController.presentationInterfaceState.chatLocation { + let tabSwitchDirection = selfController.chatDisplayNode.chatLocationTabSwitchDirection(from: selfController.chatDisplayNode.chatLocation, to: selfController.presentationInterfaceState.chatLocation) + selfController.chatDisplayNode.updateChatLocation(chatLocation: selfController.presentationInterfaceState.chatLocation, transition: transition, tabSwitchDirection: tabSwitchDirection) + } + selfController.updateSlowmodeStatus() switch updatedChatPresentationInterfaceState.inputMode { @@ -489,19 +496,25 @@ func updateChatPresentationInterfaceStateImpl( selfController.leftNavigationButton = nil } - var buttonsAnimated = transition.isAnimated - if let button = rightNavigationButtonForChatInterfaceState(context: selfController.context, presentationInterfaceState: updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { - if selfController.rightNavigationButton != button { - if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { - buttonsAnimated = false + if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum { + } else { + var buttonsAnimated = transition.isAnimated + if let button = rightNavigationButtonForChatInterfaceState(context: selfController.context, presentationInterfaceState: updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { + if selfController.rightNavigationButton != button { + if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { + buttonsAnimated = false + } + if case .replyThread = selfController.chatLocation { + buttonsAnimated = false + } + if let channel = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + buttonsAnimated = false + } + selfController.rightNavigationButton = button } - if case .replyThread = selfController.chatLocation { - buttonsAnimated = false - } - selfController.rightNavigationButton = button + } else if let _ = selfController.rightNavigationButton { + selfController.rightNavigationButton = nil } - } else if let _ = selfController.rightNavigationButton { - selfController.rightNavigationButton = nil } if let button = secondaryRightNavigationButtonForChatInterfaceState(context: selfController.context, presentationInterfaceState: updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.secondaryRightNavigationButton, target: selfController, selector: #selector(selfController.secondaryRightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { @@ -587,12 +600,10 @@ func updateChatPresentationInterfaceStateImpl( } } - if selfController.chatDisplayNode.historyNode.chatLocation != selfController.presentationInterfaceState.chatLocation { + if previousChatLocation != selfController.presentationInterfaceState.chatLocation { selfController.chatLocation = selfController.presentationInterfaceState.chatLocation - selfController.chatDisplayNode.chatLocation = selfController.presentationInterfaceState.chatLocation selfController.reloadChatLocation() selfController.reloadCachedData() - selfController.chatDisplayNode.historyNode.updateChatLocation(chatLocation: selfController.presentationInterfaceState.chatLocation) } selfController.updateDownButtonVisibility() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 20c5e037f0..af5d8d608d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -842,7 +842,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true case let .quickReplyMessageInput(_, shortcutType): if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty { - let titleString: String let textString: String switch shortcutType { @@ -7919,6 +7918,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) } } + + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + attributes.removeAll(where: { $0 is SendAsMessageAttribute }) + if channel.adminRights != nil, let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId { + attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId)) + } + } if let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId { if attributes.first(where: { $0 is SendAsMessageAttribute }) == nil { attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId)) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4b9b6368f3..fdcd28bf4f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -135,7 +135,8 @@ class HistoryNodeContainer: ASDisplayNode { class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let context: AccountContext - var chatLocation: ChatLocation + private(set) var chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic let controllerInteraction: ChatControllerInteraction private weak var controller: ChatControllerImpl? @@ -158,7 +159,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let contentContainerNode: ChatNodeContainer let contentDimNode: ASDisplayNode let backgroundNode: WallpaperBackgroundNode - let historyNode: ChatHistoryListNodeImpl + var historyNode: ChatHistoryListNodeImpl var blurredHistoryNode: ASImageNode? let historyNodeContainer: HistoryNodeContainer let loadingNode: ChatLoadingNode @@ -183,6 +184,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private(set) var validLayout: (ContainerViewLayout, CGFloat)? private var visibleAreaInset = UIEdgeInsets() + private var currentListViewLayout: (size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets)? private(set) var searchNavigationNode: ChatSearchNavigationContentNode? @@ -293,7 +295,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var dropDimNode: ASDisplayNode? - let messageTransitionNode: ChatMessageTransitionNodeImpl + var messageTransitionNode: ChatMessageTransitionNodeImpl private let presentationContextMarker = ASDisplayNode() @@ -405,6 +407,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, statusBar: StatusBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?) { self.context = context self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder self.controllerInteraction = controllerInteraction self.chatPresentationInterfaceState = chatPresentationInterfaceState self.automaticMediaDownloadSettings = automaticMediaDownloadSettings @@ -762,34 +765,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { assert(Queue.mainQueue().isCurrent()) - self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in - if let strongSelf = self { - let wasLoading = strongSelf.isLoadingValue - if case let .loading(earlier) = loadState { - strongSelf.updateIsLoading(isLoading: true, earlier: earlier, animated: animated) - } else { - strongSelf.updateIsLoading(isLoading: false, earlier: false, animated: animated) - } - - var emptyType: ChatHistoryNodeLoadState.EmptyType? - if case let .empty(type) = loadState { - if case .botInfo = type { - } else { - emptyType = type - if case .joined = type { - if strongSelf.didDisplayEmptyGreeting { - emptyType = .generic - } else { - strongSelf.didDisplayEmptyGreeting = true - } - } - } - } else if case .messages = loadState { - strongSelf.didDisplayEmptyGreeting = true - } - strongSelf.updateIsEmpty(emptyType, wasLoading: wasLoading, animated: animated) - } - } + self.setupHistoryNode() self.interactiveEmojisDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { preferencesView -> InteractiveEmojiConfiguration in @@ -801,31 +777,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { strongSelf.interactiveEmojis = emojis } }) - - var backgroundColors: [UInt32] = [] - switch chatPresentationInterfaceState.chatWallpaper { - case let .file(file): - if file.isPattern { - backgroundColors = file.settings.colors - } - case let .gradient(gradient): - backgroundColors = gradient.colors - case let .color(color): - backgroundColors = [color] - default: - break - } - if !backgroundColors.isEmpty { - let averageColor = UIColor.average(of: backgroundColors.map(UIColor.init(rgb:))) - if averageColor.hsb.b >= 0.3 { - self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) - } else { - self.historyNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) - } - } else { - self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) - } - self.historyNode.enableExtractedBackgrounds = true self.addSubnode(self.wrappingNode) self.wrappingNode.contentNode.addSubnode(self.contentContainerNode) @@ -866,8 +817,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) - self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in self?.interfaceInteraction?.presentController(controller, nil) }) @@ -1014,44 +963,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return false } - - self.displayVideoUnmuteTipDisposable = (combineLatest(queue: Queue.mainQueue(), ApplicationSpecificNotice.getVolumeButtonToUnmute(accountManager: self.context.sharedContext.accountManager), self.historyNode.hasVisiblePlayableItemNodes, self.historyNode.isInteractivelyScrolling) - |> mapToSignal { notice, hasVisiblePlayableItemNodes, isInteractivelyScrolling -> Signal in - let display = !notice && hasVisiblePlayableItemNodes && !isInteractivelyScrolling - if display { - return .complete() - |> delay(2.5, queue: Queue.mainQueue()) - |> then( - .single(display) - ) - } else { - return .single(display) - } - }).startStrict(next: { [weak self] display in - if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - if display { - var nodes: [(CGFloat, ChatMessageItemView, ASDisplayNode)] = [] - var skip = false - strongSelf.historyNode.forEachVisibleItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, isVideoMessage, _, badgeNode) = itemNode.playMediaWithSound(), let node = badgeNode { - if soundEnabled { - skip = true - } else if !skip && !isVideoMessage, case let .visible(fraction, _) = itemNode.visibility { - nodes.insert((fraction, itemNode, node), at: 0) - } - } - } - for (fraction, _, badgeNode) in nodes { - if fraction > 0.7 { - interfaceInteraction.displayVideoUnmuteTip(badgeNode.view.convert(badgeNode.view.bounds, to: strongSelf.view).origin.offsetBy(dx: 42.0, dy: -1.0)) - break - } - } - } else { - interfaceInteraction.displayVideoUnmuteTip(nil) - } - } - }) } private func updateIsEmpty(_ emptyType: ChatHistoryNodeLoadState.EmptyType?, wasLoading: Bool, animated: Bool) { @@ -2277,6 +2188,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + self.currentListViewLayout = (contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets) listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets, duration: duration, curve: curve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems, customAnimationTransition: customListAnimationTransition), additionalScrollDistance, scrollToTop, { [weak self] in if let strongSelf = self { strongSelf.notifyTransitionCompletionListeners(transition: transition) @@ -4881,4 +4793,199 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateSublayerTransformOffset(layer: inputPanelNode.layer, offset: CGPoint(x: 0.0, y: overscrollNode == nil ? 0.0 : -5.0)) } } + + private func setupHistoryNode() { + var backgroundColors: [UInt32] = [] + switch self.chatPresentationInterfaceState.chatWallpaper { + case let .file(file): + if file.isPattern { + backgroundColors = file.settings.colors + } + case let .gradient(gradient): + backgroundColors = gradient.colors + case let .color(color): + backgroundColors = [color] + default: + break + } + if !backgroundColors.isEmpty { + let averageColor = UIColor.average(of: backgroundColors.map(UIColor.init(rgb:))) + if averageColor.hsb.b >= 0.3 { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) + } else { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) + } + } else { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) + } + self.historyNode.enableExtractedBackgrounds = true + + self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in + guard let strongSelf = self else { + return + } + let wasLoading = strongSelf.isLoadingValue + if case let .loading(earlier) = loadState { + strongSelf.updateIsLoading(isLoading: true, earlier: earlier, animated: animated) + } else { + strongSelf.updateIsLoading(isLoading: false, earlier: false, animated: animated) + } + + var emptyType: ChatHistoryNodeLoadState.EmptyType? + if case let .empty(type) = loadState { + if case .botInfo = type { + } else { + emptyType = type + if case .joined = type { + if strongSelf.didDisplayEmptyGreeting { + emptyType = .generic + } else { + strongSelf.didDisplayEmptyGreeting = true + } + } + } + } else if case .messages = loadState { + strongSelf.didDisplayEmptyGreeting = true + } + strongSelf.updateIsEmpty(emptyType, wasLoading: wasLoading, animated: animated) + } + + self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.displayVideoUnmuteTipDisposable?.dispose() + self.displayVideoUnmuteTipDisposable = (combineLatest(queue: Queue.mainQueue(), ApplicationSpecificNotice.getVolumeButtonToUnmute(accountManager: self.context.sharedContext.accountManager), self.historyNode.hasVisiblePlayableItemNodes, self.historyNode.isInteractivelyScrolling) + |> mapToSignal { notice, hasVisiblePlayableItemNodes, isInteractivelyScrolling -> Signal in + let display = !notice && hasVisiblePlayableItemNodes && !isInteractivelyScrolling + if display { + return .complete() + |> delay(2.5, queue: Queue.mainQueue()) + |> then( + .single(display) + ) + } else { + return .single(display) + } + }).startStrict(next: { [weak self] display in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + if display { + var nodes: [(CGFloat, ChatMessageItemView, ASDisplayNode)] = [] + var skip = false + strongSelf.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, isVideoMessage, _, badgeNode) = itemNode.playMediaWithSound(), let node = badgeNode { + if soundEnabled { + skip = true + } else if !skip && !isVideoMessage, case let .visible(fraction, _) = itemNode.visibility { + nodes.insert((fraction, itemNode, node), at: 0) + } + } + } + for (fraction, _, badgeNode) in nodes { + if fraction > 0.7 { + interfaceInteraction.displayVideoUnmuteTip(badgeNode.view.convert(badgeNode.view.bounds, to: strongSelf.view).origin.offsetBy(dx: 42.0, dy: -1.0)) + break + } + } + } else { + interfaceInteraction.displayVideoUnmuteTip(nil) + } + } + }) + } + + func chatLocationTabSwitchDirection(from fromLocation: ChatLocation, to toLocation: ChatLocation) -> Bool? { + var leftIndex: Int? + var rightIndex: Int? + if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode { + leftIndex = titleTopicsAccessoryPanelNode.topicIndex(threadId: fromLocation.threadId) + rightIndex = titleTopicsAccessoryPanelNode.topicIndex(threadId: toLocation.threadId) + } else if let leftPanelView = self.leftPanel?.view.view as? ChatSideTopicsPanel.View { + leftIndex = leftPanelView.topicIndex(threadId: fromLocation.threadId) + rightIndex = leftPanelView.topicIndex(threadId: toLocation.threadId) + } + guard let leftIndex, let rightIndex else { + return nil + } + return leftIndex < rightIndex + } + + func updateChatLocation(chatLocation: ChatLocation, transition: ContainedViewLayoutTransition, tabSwitchDirection: Bool?) { + if chatLocation == self.chatLocation { + return + } + self.chatLocation = chatLocation + + let historyNode = ChatHistoryListNodeImpl( + context: self.context, + updatedPresentationData: self.controller?.updatedPresentationData ?? (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), + chatLocation: chatLocation, + chatLocationContextHolder: self.chatLocationContextHolder, + tag: nil, + source: .default, + subject: nil, + controllerInteraction: self.controllerInteraction, + selectedMessages: self.selectedMessagesPromise.get(), + rotated: self.controllerInteraction.chatIsRotated, + isChatPreview: false, + messageTransitionNode: { [weak self] in + return self?.messageTransitionNode + } + ) + + var getContentAreaInScreenSpaceImpl: (() -> CGRect)? + var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? + let messageTransitionNode = ChatMessageTransitionNodeImpl(listNode: historyNode, getContentAreaInScreenSpace: { + return getContentAreaInScreenSpaceImpl?() ?? CGRect() + }, onTransitionEvent: { transition in + onTransitionEventImpl?(transition) + }) + + getContentAreaInScreenSpaceImpl = { [weak self] in + guard let strongSelf = self else { + return CGRect() + } + + return strongSelf.view.convert(strongSelf.frameForVisibleArea(), to: nil) + } + + onTransitionEventImpl = { [weak self] transition in + guard let strongSelf = self else { + return + } + if (strongSelf.context.sharedContext.currentPresentationData.with({ $0 })).reduceMotion { + return + } + if strongSelf.context.sharedContext.energyUsageSettings.fullTranslucency { + strongSelf.backgroundNode.animateEvent(transition: transition, extendAnimation: false) + } + } + + self.wrappingNode.contentNode.insertSubnode(messageTransitionNode, aboveSubnode: self.messageTransitionNode) + self.messageTransitionNode.removeFromSupernode() + self.messageTransitionNode = messageTransitionNode + + let previousHistoryNode = self.historyNode + previousHistoryNode.supernode?.insertSubnode(historyNode, aboveSubnode: previousHistoryNode) + self.historyNode = historyNode + + self.setupHistoryNode() + + historyNode.position = previousHistoryNode.position + historyNode.bounds = previousHistoryNode.bounds + historyNode.transform = previousHistoryNode.transform + + if let currentListViewLayout = self.currentListViewLayout { + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: currentListViewLayout.size, insets: currentListViewLayout.insets, scrollIndicatorInsets: currentListViewLayout.scrollIndicatorInsets, duration: 0.0, curve: .Default(duration: nil), ensureTopInsetForOverlayHighlightedItems: nil, customAnimationTransition: nil) + historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false, completion: {}) + } + + if let validLayout = self.validLayout, transition.isAnimated, let tabSwitchDirection { + let offsetMultiplier: CGFloat = tabSwitchDirection ? 1.0 : -1.0 + transition.animatePosition(layer: historyNode.layer, from: CGPoint(x: offsetMultiplier * validLayout.0.size.width, y: 0.0), to: CGPoint(), removeOnCompletion: true, additive: true) + transition.animatePosition(layer: previousHistoryNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier * validLayout.0.size.width, y: 0.0), removeOnCompletion: false, additive: true, completion: { [weak previousHistoryNode] _ in + previousHistoryNode?.removeFromSupernode() + }) + } else { + previousHistoryNode.removeFromSupernode() + } + } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3c2d528aff..b7c174c88c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1233,14 +1233,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false) } - public func updateChatLocation(chatLocation: ChatLocation) { - if self.chatLocation == chatLocation { - return - } - self.chatLocation = chatLocation - self.beginChatHistoryTransitions(resetScrolling: false, switchedToAnotherSource: true) - } - private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) { self.adMessagesDisposable = (adMessages |> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in @@ -2362,7 +2354,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.dynamicBounceEnabled = false strongSelf.forEachItemHeaderNode { itemHeaderNode in - if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { + if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNodeImpl { dateNode.updatePresentationData(chatPresentationData, context: strongSelf.context) } else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNodeImpl { avatarNode.updatePresentationData(chatPresentationData, context: strongSelf.context) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index f6556dd621..1571b29684 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -231,15 +231,16 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if channel.flags.contains(.isMonoforum) { if channel.adminRights != nil, case .peer = chatPresentationInterfaceState.chatLocation { - displayInputTextPanel = false - - if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { - return (currentPanel, nil) - } else { - let panel = ChatRestrictedInputPanelNode() - panel.context = context - panel.interfaceInteraction = interfaceInteraction - return (panel, nil) + if chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil { + displayInputTextPanel = false + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } } } else { displayInputTextPanel = true diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 81470ba529..c704f1c010 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -90,8 +90,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) } if let channel = interfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, channel.isMonoForum { - //TODO:localize - self.textNode.attributedText = NSAttributedString(string: "Choose a thread to reply", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelForumModeReplyText, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let _ = accountFreezeConfiguration?.freezeUntilDate { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelFrozenAccount_Title, font: Font.semibold(15.0), textColor: interfaceState.theme.list.itemDestructiveColor) self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelFrozenAccount_Text, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) diff --git a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift index cfb4adc56d..bdd5503820 100644 --- a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift @@ -868,4 +868,16 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0)) } } + + public func topicIndex(threadId: Int64?) -> Int? { + if let threadId { + if let value = self.items.firstIndex(where: { $0.id == .chatList(PeerId(threadId)) }) { + return value + 1 + } else { + return nil + } + } else { + return 0 + } + } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index c54bed8408..e9c49ce330 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2409,7 +2409,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { - return ChatMessageDateHeader(timestamp: timestamp, scheduled: false, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), controllerInteraction: nil, context: context) + return ChatMessageDateHeader(timestamp: timestamp, separableThreadId: nil, scheduled: false, displayPeer: nil, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), controllerInteraction: nil, context: context) } public func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { diff --git a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift index 641b903f99..9f6133b1d6 100644 --- a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift +++ b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift @@ -297,7 +297,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { headerNode.item = header } headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate) } else { headerNode = header.node(synchronousLoad: true) if headerNode.item !== header { @@ -309,7 +309,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.itemHeaderNodes[id] = headerNode strongSelf.containerNode.addSubnode(headerNode) - headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate) } } } diff --git a/versions.json b/versions.json index a7ac604a44..ae9a377e5e 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "app": "11.11", - "xcode": "16.2", + "xcode": "16.3", "bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3", "macos": "15" }