[WIP] Monoforums

This commit is contained in:
Isaac 2025-05-14 00:50:46 +08:00
parent 9e18743b1b
commit f90402102b
36 changed files with 1191 additions and 340 deletions

View File

@ -204,6 +204,7 @@ private enum ChatListSearchItemHeaderId: Hashable {
public final class ChatListSearchItemHeader: ListViewItemHeader { public final class ChatListSearchItemHeader: ListViewItemHeader {
public let id: ListViewItemNode.HeaderId public let id: ListViewItemNode.HeaderId
public let stackingId: ListViewItemNode.HeaderId? = nil
public let type: ChatListSearchItemHeaderType public let type: ChatListSearchItemHeaderType
public let stickDirection: ListViewItemHeaderStickDirection = .top public let stickDirection: ListViewItemHeaderStickDirection = .top
public let stickOverInsets: Bool = true public let stickOverInsets: Bool = true

View File

@ -6,6 +6,7 @@ import ListSectionHeaderNode
final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { final class ContactListNameIndexHeader: Equatable, ListViewItemHeader {
let id: ListViewItemNode.HeaderId let id: ListViewItemNode.HeaderId
let stackingId: ListViewItemNode.HeaderId? = nil
let theme: PresentationTheme let theme: PresentationTheme
let letter: unichar let letter: unichar
let stickDirection: ListViewItemHeaderStickDirection = .top let stickDirection: ListViewItemHeaderStickDirection = .top

View File

@ -9,6 +9,11 @@ private let insertionAnimationDuration: Double = 0.4
private struct VisibleHeaderNodeId: Hashable { private struct VisibleHeaderNodeId: Hashable {
var id: ListViewItemNode.HeaderId var id: ListViewItemNode.HeaderId
var affinity: Int var affinity: Int
init(id: ListViewItemNode.HeaderId, affinity: Int) {
self.id = id
self.affinity = affinity
}
} }
private final class ListViewBackingLayer: CALayer { 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) { func addHeader(id: VisibleHeaderNodeId, upperBound: CGFloat, upperIndex: Int, upperBoundEdge: CGFloat, lowerBound: CGFloat, lowerIndex: Int, item: ListViewItemHeader, hasValidNodes: Bool) {
let itemHeaderHeight: CGFloat = item.height let itemHeaderHeight: CGFloat = item.height
let headerFrame: CGRect var insertItemBelowOtherHeaders = false
let stickLocationDistanceFactor: CGFloat var offsetByHeaderNodeId: ListViewItemNode.HeaderId?
let stickLocationDistance: CGFloat var didOffsetByHeaderNode = false
var headerFrame: CGRect
let naturalY: CGFloat
var stickLocationDistanceFactor: CGFloat = 0.0
var stickLocationDistance: CGFloat
switch item.stickDirection { switch item.stickDirection {
case .top: 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)) 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 stickLocationDistance = headerFrame.minY - upperBound
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .topEdge: 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)) 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 stickLocationDistance = headerFrame.minY - upperBoundEdge + itemHeaderHeight
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .bottom: 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)) 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 stickLocationDistance = lowerBound - headerFrame.maxY
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) 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) visibleHeaderNodes.append(id)
let initialHeaderNodeAlpha = self.itemHeaderNodesAlpha let initialHeaderNodeAlpha = self.itemHeaderNodesAlpha
@ -3896,7 +3962,14 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
headerNode = current headerNode = current
switch transition.0 { switch transition.0 {
case .immediate: case .immediate:
let previousFrame = headerNode.frame
headerNode.updateFrame(headerFrame, within: self.visibleSize) 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): case let .animated(duration, curve):
let previousFrame = headerNode.frame let previousFrame = headerNode.frame
headerNode.updateFrame(headerFrame, within: self.visibleSize) headerNode.updateFrame(headerFrame, within: self.visibleSize)
@ -3927,7 +4000,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
item.updateNode(headerNode, previous: nil, next: nil) item.updateNode(headerNode, previous: nil, next: nil)
headerNode.item = item 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.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: true)
headerNode.internalStickLocationDistance = stickLocationDistance headerNode.internalStickLocationDistance = stickLocationDistance
if !hasValidNodes && !headerNode.alpha.isZero { if !hasValidNodes && !headerNode.alpha.isZero {
@ -3940,7 +4013,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
headerNode.animateAdded(duration: 0.2) headerNode.animateAdded(duration: 0.2)
} }
} }
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0) headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: stickLocationDistance, transition: transition.0)
} else { } else {
headerNode = item.node(synchronousLoad: synchronousLoad) headerNode = item.node(synchronousLoad: synchronousLoad)
headerNode.alpha = initialHeaderNodeAlpha headerNode.alpha = initialHeaderNodeAlpha
@ -3950,10 +4023,28 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
} }
headerNode.updateFlashingOnScrolling(flashing, animated: false) headerNode.updateFlashingOnScrolling(flashing, animated: false)
headerNode.frame = headerFrame 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) headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: false)
self.itemHeaderNodes[id] = headerNode 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) self.insertSubnode(headerNode, belowSubnode: verticalScrollIndicator)
} else { } else {
self.addSubnode(headerNode) self.addSubnode(headerNode)
@ -3962,8 +4053,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
headerNode.alpha = initialHeaderNodeAlpha headerNode.alpha = initialHeaderNodeAlpha
headerNode.animateAdded(duration: 0.2) 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)? var maxIntersectionHeight: (CGFloat, Int)?
for i in upperIndex ... lowerIndex { for i in upperIndex ... lowerIndex {
let itemNode = self.itemNodes[i] 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)] = [:] 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 { for phase in 0 ..< 2 {
let itemNode = self.itemNodes[i] for i in 0 ..< self.itemNodes.count {
let itemFrame = itemNode.apparentFrame let itemNode = self.itemNodes[i]
let itemTopInset = itemNode.insets.top let itemFrame = itemNode.apparentFrame
var validItemHeaderSpaces: [AnyHashable] = [] let itemTopInset = itemNode.insets.top
if let itemHeaders = itemNode.headers() { var validItemHeaderSpaces: [AnyHashable] = []
for itemHeader in itemHeaders { if let itemHeaders = itemNode.headers() {
guard let affinity = itemNode.headerSpaceAffinities[itemHeader.id] else { outerItemHeaders: for itemHeader in itemHeaders {
assertionFailure() if phase == 0 {
continue if itemHeader.stackingId != nil {
} continue outerItemHeaders
}
let headerId = VisibleHeaderNodeId(id: itemHeader.id, affinity: affinity) } else {
if itemHeader.stackingId == nil {
validItemHeaderSpaces.append(itemHeader.id.space) continue outerItemHeaders
}
let itemMaxY: CGFloat }
if itemHeader.stickOverInsets {
itemMaxY = itemFrame.maxY guard let affinity = itemNode.headerSpaceAffinities[itemHeader.id] else {
} else { assertionFailure()
itemMaxY = itemFrame.maxY - (self.rotated ? itemNode.insets.top : itemNode.insets.bottom) continue
} }
if let (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, previousLowerBound, previousLowerIndex, previousHeaderItem, hasValidNodes) = previousHeaderBySpace[itemHeader.id.space] { let headerId = VisibleHeaderNodeId(id: itemHeader.id, affinity: affinity)
if previousHeaderId == headerId {
previousHeaderBySpace[itemHeader.id.space] = (previousHeaderId, previousUpperBound, previousUpperIndex, previousUpperBoundEdge, itemMaxY, i, previousHeaderItem, hasValidNodes || itemNode.index != nil) 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 { } 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) 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 {
for (space, previousHeader) in previousHeaderBySpace { if validItemHeaderSpaces.contains(space) {
if validItemHeaderSpaces.contains(space) { continue
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)
} }
} }

View File

@ -10,6 +10,7 @@ public enum ListViewItemHeaderStickDirection {
public protocol ListViewItemHeader: AnyObject { public protocol ListViewItemHeader: AnyObject {
var id: ListViewItemNode.HeaderId { get } var id: ListViewItemNode.HeaderId { get }
var stackingId: ListViewItemNode.HeaderId? { get }
var stickDirection: ListViewItemHeaderStickDirection { get } var stickDirection: ListViewItemHeaderStickDirection { get }
var height: CGFloat { get } var height: CGFloat { get }
var stickOverInsets: Bool { get } var stickOverInsets: Bool { get }
@ -29,6 +30,9 @@ open class ListViewItemHeaderNode: ASDisplayNode {
private var isFlashingOnScrolling = false private var isFlashingOnScrolling = false
weak var attachedToItemNode: ListViewItemNode? weak var attachedToItemNode: ListViewItemNode?
var offsetByHeaderNodeId: ListViewItemNode.HeaderId?
var naturalOriginY: CGFloat?
public var item: ListViewItemHeader? public var item: ListViewItemHeader?
func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) {
@ -61,7 +65,7 @@ open class ListViewItemHeaderNode: ASDisplayNode {
self.isLayerBacked = layerBacked self.isLayerBacked = layerBacked
} }
open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { open func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) {
} }
final func addScrollingOffset(_ scrollingOffset: CGFloat) { final func addScrollingOffset(_ scrollingOffset: CGFloat) {

View File

@ -2045,6 +2045,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
public final class ItemListPeerItemHeader: ListViewItemHeader { public final class ItemListPeerItemHeader: ListViewItemHeader {
public let id: ListViewItemNode.HeaderId public let id: ListViewItemNode.HeaderId
public let stackingId: ListViewItemNode.HeaderId? = nil
public let context: AccountContext public let context: AccountContext
public let text: NSAttributedString public let text: NSAttributedString
public let additionalText: String 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) 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 { if self.stickDistanceFactor == factor {
return return
} }

View File

@ -42,6 +42,7 @@ final class ListMessageDateHeader: ListViewItemHeader {
private let year: Int32 private let year: Int32
let id: ListViewItemNode.HeaderId let id: ListViewItemNode.HeaderId
let stackingId: ListViewItemNode.HeaderId? = nil
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let fontSize: PresentationFontSize let fontSize: PresentationFontSize

View File

@ -2776,6 +2776,10 @@ final class MessageHistoryTable: Table {
associatedThreadInfo = self.seedConfiguration.decodeMessageThreadInfo(data.data) 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) 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)
} }

View File

@ -498,6 +498,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
} }
} }
} }
switch message { switch message {
case let .message(_, attributes, _, _, threadId, replyToMessageId, _, _, _, _): 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) { if let replyToMessageId = replyToMessageId, (replyToMessageId.messageId.peerId != peerId && peerId.namespace == Namespaces.Peer.SecretChat), let replyMessage = transaction.getMessage(replyToMessageId.messageId) {

View File

@ -583,9 +583,6 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
} }
} }
} else if let inputChannel = maybePeer.flatMap(apiInputChannel) { } 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)) let fullChannelSignal = network.request(Api.functions.channels.getFullChannel(channel: inputChannel))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { error -> Signal<Api.messages.ChatFull?, NoError> in |> `catch` { error -> Signal<Api.messages.ChatFull?, NoError> in
@ -594,10 +591,17 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
} }
return .single(nil) return .single(nil)
} }
let participantSignal = network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf))
|> map(Optional.init)
|> `catch` { error -> Signal<Api.channels.ChannelParticipant?, NoError> in let participantSignal: Signal<Api.channels.ChannelParticipant?, NoError>
return .single(nil) 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<Api.channels.ChannelParticipant?, NoError> in
return .single(nil)
}
} }
return combineLatest(fullChannelSignal, participantSignal) return combineLatest(fullChannelSignal, participantSignal)

View File

@ -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) var displaySize = CGSize(width: 180.0, height: 180.0)
let telegramFile = self.telegramFile let telegramFile = self.telegramFile
let emojiFile = self.emojiFile let emojiFile = self.emojiFile
@ -823,7 +823,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
func continueAsyncLayout(_ weakSelf: Weak<ChatMessageAnimatedStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { func continueAsyncLayout(_ weakSelf: Weak<ChatMessageAnimatedStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) 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) 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 { if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight
} else {
if dateHeaderAtBottom.hasDate {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
if dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
} }
var deliveryFailedInset: CGFloat = 0.0 var deliveryFailedInset: CGFloat = 0.0
@ -1377,6 +1384,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.updateAccessibilityData(accessibilityData) strongSelf.updateAccessibilityData(accessibilityData)
strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic)
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.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) 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) return continueAsyncLayout(weakSelf, item, params, mergedTop, mergedBottom, dateHeaderAtBottom)
} }
} }

View File

@ -728,7 +728,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var forceStopAnimations: Bool = false 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 currentInputParams: Params?
private var currentApplyParams: ListViewItemApply? 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))))] = [] 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 { for contentNode in self.contentNodes {
if let message = contentNode.item?.message { if let message = contentNode.item?.message {
@ -1431,7 +1431,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) 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, currentContentClassesPropertiesAndLayouts: currentContentClassesPropertiesAndLayouts,
authorNameLayout: authorNameLayout, authorNameLayout: authorNameLayout,
viaMeasureLayout: viaMeasureLayout, viaMeasureLayout: viaMeasureLayout,
@ -1456,11 +1462,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private static func beginLayout( private static func beginLayout(
selfReference: Weak<ChatMessageBubbleItemNode>, selfReference: Weak<ChatMessageBubbleItemNode>,
_ item: ChatMessageItem, item: ChatMessageItem,
_ params: ListViewItemLayoutParams, params: ListViewItemLayoutParams,
_ mergedTop: ChatMessageMerge, mergedTop: ChatMessageMerge,
_ mergedBottom: ChatMessageMerge, mergedBottom: ChatMessageMerge,
_ dateHeaderAtBottom: Bool, 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))))], 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), authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
@ -2620,7 +2626,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
var hasThreadInfo = false 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 hasThreadInfo = true
} else if case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind { } 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 { 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) 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 { if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight
} } else {
if isAd { if dateHeaderAtBottom.hasDate {
layoutInsets.top += 4.0 layoutInsets.top += layoutConstants.timestampHeaderHeight
}
if dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
if isAd {
layoutInsets.top += 4.0
}
} }
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) 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.updateAttachedAvatarNodeOffset(offset: avatarOffset, transition: .animated(duration: 0.3, curve: .spring))
} }
strongSelf.updateAttachedAvatarNodeIsHidden(isHidden: isSidePanelOpen, transition: animation.transition) 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()) let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
if isFailed { if isFailed {

View File

@ -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 layoutConstants = self.layoutConstants
let makeVideoLayout = self.interactiveVideoNode.asyncLayout() let makeVideoLayout = self.interactiveVideoNode.asyncLayout()
@ -296,7 +296,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
let currentForwardInfo = self.appliedForwardInfo let currentForwardInfo = self.appliedForwardInfo
let currentPlaying = self.appliedCurrentlyPlaying let currentPlaying = self.appliedCurrentlyPlaying
func continueAsyncLayout(_ weakSelf: Weak<ChatMessageInstantVideoItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { func continueAsyncLayout(_ weakSelf: Weak<ChatMessageInstantVideoItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
@ -391,8 +391,15 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
} }
var layoutInsets = layoutConstants.instantVideo.insets var layoutInsets = layoutConstants.instantVideo.insets
if dateHeaderAtBottom { if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight
} else {
if dateHeaderAtBottom.hasDate {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
if dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
} }
var deliveryFailedInset: CGFloat = 0.0 var deliveryFailedInset: CGFloat = 0.0

View File

@ -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 { public protocol ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
func updateSelectionState(animated: Bool) func updateSelectionState(animated: Bool)
func updateAvatarIsHidden(isHidden: Bool, transition: ContainedViewLayoutTransition) func updateAvatarIsHidden(isHidden: Bool, transition: ContainedViewLayoutTransition)
@ -128,7 +142,7 @@ public protocol ChatMessageItem: ListViewItem {
var sending: Bool { get } var sending: Bool { get }
var failed: 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 { public func hasCommentButton(item: ChatMessageItem) -> Bool {

View File

@ -108,6 +108,7 @@ public struct ChatMessageItemWallpaperLayoutConstants {
public struct ChatMessageItemLayoutConstants { public struct ChatMessageItemLayoutConstants {
public var avatarDiameter: CGFloat public var avatarDiameter: CGFloat
public var timestampHeaderHeight: CGFloat public var timestampHeaderHeight: CGFloat
public var timestampDateAndTopicHeaderHeight: CGFloat
public var bubble: ChatMessageItemBubbleLayoutConstants public var bubble: ChatMessageItemBubbleLayoutConstants
public var image: ChatMessageItemImageLayoutConstants public var image: ChatMessageItemImageLayoutConstants
@ -117,9 +118,10 @@ public struct ChatMessageItemLayoutConstants {
public var instantVideo: ChatMessageItemInstantVideoConstants public var instantVideo: ChatMessageItemInstantVideoConstants
public var wallpapers: ChatMessageItemWallpaperLayoutConstants 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.avatarDiameter = avatarDiameter
self.timestampHeaderHeight = timestampHeaderHeight self.timestampHeaderHeight = timestampHeaderHeight
self.timestampDateAndTopicHeaderHeight = timestampDateAndTopicHeaderHeight
self.bubble = bubble self.bubble = bubble
self.image = image self.image = image
self.video = video 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 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) 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 { 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 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) 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)
} }
} }

View File

@ -16,6 +16,7 @@ import WallpaperBackgroundNode
import ChatControllerInteraction import ChatControllerInteraction
import AvatarVideoNode import AvatarVideoNode
import ChatMessageItem import ChatMessageItem
import AvatarNode
private let timezoneOffset: Int32 = { private let timezoneOffset: Int32 = {
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
@ -28,25 +29,55 @@ private let timezoneOffset: Int32 = {
private let granularity: Int32 = 60 * 60 * 24 private let granularity: Int32 = 60 * 60 * 24
public final class ChatMessageDateHeader: ListViewItemHeader { 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 timestamp: Int32
private let roundedTimestamp: Int32 private let roundedTimestamp: Int32
private let scheduled: Bool private let scheduled: Bool
public let displayPeer: PeerData?
public let id: ListViewItemNode.HeaderId public let id: ListViewItemNode.HeaderId
public let stackingId: ListViewItemNode.HeaderId?
public let idValue: Id
public let presentationData: ChatPresentationData public let presentationData: ChatPresentationData
public let controllerInteraction: ChatControllerInteraction? public let controllerInteraction: ChatControllerInteraction?
public let context: AccountContext public let context: AccountContext
public let action: ((Int32, Bool) -> Void)? 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.timestamp = timestamp
self.scheduled = scheduled self.scheduled = scheduled
self.displayPeer = displayPeer
self.presentationData = presentationData self.presentationData = presentationData
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.context = context self.context = context
self.action = action self.action = action
self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) 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 let isRotated = controllerInteraction?.chatIsRotated ?? true
@ -67,11 +98,11 @@ public final class ChatMessageDateHeader: ListViewItemHeader {
} }
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { 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?) { 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 return
} }
node.updatePresentationData(next.presentationData, context: next.context) 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 labelNode: TextNode
public let backgroundNode: NavigationBackgroundNode public let backgroundNode: NavigationBackgroundNode
public let stickBackgroundNode: ASImageNode public let stickBackgroundNode: ASImageNode
private var backgroundContent: WallpaperBubbleBackgroundNode? private var backgroundContent: WallpaperBubbleBackgroundNode?
private var text: String
private let localTimestamp: Int32 init(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, localTimestamp: Int32, scheduled: Bool) {
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) {
self.presentationData = presentationData self.presentationData = presentationData
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.context = context
self.localTimestamp = localTimestamp
self.action = action
self.labelNode = TextNode() self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
@ -195,13 +265,7 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
} }
self.text = text self.text = text
let isRotated = controllerInteraction?.chatIsRotated ?? true super.init()
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) 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.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.image = graphics.dateFloatingBackground
self.stickBackgroundNode.alpha = 0.0 self.stickBackgroundNode.alpha = 0.0
if let backgroundContent = self.backgroundContent { if let backgroundContent = self.backgroundContent {
self.addSubnode(backgroundContent) self.addSubnode(backgroundContent)
} else { } else {
@ -227,14 +291,8 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
let _ = apply() let _ = apply()
self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) 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 let previousPresentationData = self.presentationData
self.presentationData = presentationData self.presentationData = presentationData
@ -256,32 +314,21 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
if presentationData.fontSize != previousPresentationData.fontSize { if presentationData.fontSize != previousPresentationData.fontSize {
self.labelNode.bounds = CGRect(origin: CGPoint(), size: size.size) 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) self.backgroundNode.updateColor(color: color, enableBlur: enableBlur, transition: .immediate)
} }
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
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) {
let chatDateSize: CGFloat = 20.0 let chatDateSize: CGFloat = 20.0
let chatDateInset: CGFloat = 6.0 let chatDateInset: CGFloat = 6.0
let labelSize = self.labelNode.bounds.size let labelSize = self.labelNode.bounds.size
let backgroundSize = CGSize(width: labelSize.width + chatDateInset * 2.0, height: chatDateSize) 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.stickBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: transition) 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) transition.updateFrame(node: backgroundContent, frame: self.backgroundNode.frame)
backgroundContent.cornerRadius = backgroundFrame.size.height / 2.0 backgroundContent.cornerRadius = backgroundFrame.size.height / 2.0
if let (rect, containerSize) = self.absolutePosition { /*if let (rect, containerSize) = self.absolutePosition {
var backgroundFrame = backgroundContent.frame var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += containerSize.height - rect.minY backgroundFrame.origin.y += containerSize.height - rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) 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) { 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 wasZero = self.stickDistanceFactor < 0.5
let isZero = factor < 0.5 let isZero = factor < 0.5
@ -322,6 +664,10 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
self.updateFlashing(animated: animated) 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) { 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 flashing = self.flashingOnScrolling || self.stickDistanceFactor < 0.5
let alpha: CGFloat = flashing ? 1.0 : 0.0 let alpha: CGFloat = flashing ? 1.0 : 0.0
let previousAlpha = self.backgroundNode.alpha
if !previousAlpha.isEqual(to: alpha) { if let dateContentNode = self.dateContentNode {
self.backgroundContent?.alpha = alpha let previousAlpha = dateContentNode.alpha
self.backgroundNode.alpha = alpha
self.labelNode.alpha = alpha if !previousAlpha.isEqual(to: alpha) {
if animated { dateContentNode.alpha = alpha
let duration: Double = flashing ? 0.3 : 0.4 if animated {
self.backgroundContent?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) let duration: Double = flashing ? 0.3 : 0.4
self.backgroundNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) dateContentNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration)
self.labelNode.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 { 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? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) { if !self.bounds.contains(point) {
return nil return nil
} }
if self.labelNode.alpha.isZero { if let dateContentNode = self.dateContentNode {
return nil 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) { if let peerContentNode = self.peerContentNode {
return self.view 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 return nil
} }
@ -383,6 +777,7 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader {
} }
public let id: ListViewItemNode.HeaderId public let id: ListViewItemNode.HeaderId
public let stackingId: ListViewItemNode.HeaderId? = nil
public let peerId: PeerId public let peerId: PeerId
public let peer: Peer? public let peer: Peer?
public let messageReference: MessageReference? 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) 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) { override public func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) {

View File

@ -220,6 +220,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
public let additionalContent: ChatMessageItemAdditionalContent? public let additionalContent: ChatMessageItemAdditionalContent?
let dateHeader: ChatMessageDateHeader let dateHeader: ChatMessageDateHeader
let topicHeader: ChatMessageDateHeader?
let avatarHeader: ChatMessageAvatarHeader? let avatarHeader: ChatMessageAvatarHeader?
public let headers: [ListViewItemHeader] public let headers: [ListViewItemHeader]
@ -286,6 +287,8 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
var displayAuthorInfo: Bool var displayAuthorInfo: Bool
let messagePeerId: PeerId = chatLocation.peerId ?? content.firstMessage.id.peerId let messagePeerId: PeerId = chatLocation.peerId ?? content.firstMessage.id.peerId
var headerSeparableThreadId: Int64?
var headerDisplayPeer: ChatMessageDateHeader.PeerData?
do { do {
let peerId = messagePeerId 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 let channel = content.firstMessage.peers[content.firstMessage.id.peerId] as? TelegramChannel, channel.isMonoForum {
if case .replyThread = chatLocation { if case .replyThread = chatLocation {
displayAuthorInfo = false 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 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 var calendar = NSCalendar.current
calendar.timeZone = TimeZone(abbreviation: "UTC")! calendar.timeZone = TimeZone(abbreviation: "UTC")!
let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) 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 { if displayAuthorInfo {
let message = content.firstMessage let message = content.firstMessage
var hasActionMedia = false var hasActionMedia = false
@ -395,6 +412,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
var headers: [ListViewItemHeader] = [] var headers: [ListViewItemHeader] = []
if !self.disableDate { if !self.disableDate {
headers.append(self.dateHeader) headers.append(self.dateHeader)
if let topicHeader = self.topicHeader {
headers.append(topicHeader)
}
} }
if case .messageOptions = associatedData.subject { if case .messageOptions = associatedData.subject {
headers = [] 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.contentSize = layout.contentSize
node.insets = layout.insets 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 top = top
var bottom = bottom var bottom = bottom
if !isRotated { if !isRotated {
@ -537,7 +557,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
var mergedTop: ChatMessageMerge = .none var mergedTop: ChatMessageMerge = .none
var mergedBottom: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none
var dateAtBottom = false var dateAtBottom = ChatMessageHeaderSpec(hasDate: false, hasTopic: false)
if let top = top as? ChatMessageItemImpl { if let top = top as? ChatMessageItemImpl {
if top.dateHeader.id != self.dateHeader.id { if top.dateHeader.id != self.dateHeader.id {
mergedBottom = .none mergedBottom = .none
@ -548,20 +568,35 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
if let bottom = bottom as? ChatMessageItemImpl { if let bottom = bottom as? ChatMessageItemImpl {
if bottom.dateHeader.id != self.dateHeader.id { if bottom.dateHeader.id != self.dateHeader.id {
mergedTop = .none mergedTop = .none
dateAtBottom = true dateAtBottom.hasDate = true
} else { }
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) mergedTop = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, bottom.message, message)
} }
} else if let bottom = bottom as? ChatUnreadItem { } else if let bottom = bottom as? ChatUnreadItem {
if bottom.header.id != self.dateHeader.id { 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 { } else if let bottom = bottom as? ChatReplyCountItem {
if bottom.header.id != self.dateHeader.id { if bottom.header.id != self.dateHeader.id {
dateAtBottom = true dateAtBottom.hasDate = true
}
if self.topicHeader != nil {
dateAtBottom.hasTopic = true
} }
} else { } else {
dateAtBottom = true dateAtBottom.hasDate = true
if self.topicHeader != nil {
dateAtBottom.hasTopic = true
}
} }
return (mergedTop, mergedBottom, dateAtBottom) 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 { Queue.mainQueue().async {
completion(layout, { info in completion(layout, { info in
apply(animation, info, false) apply(animation, info, false)

View File

@ -25,7 +25,7 @@ public class ChatReplyCountItem: ListViewItem {
self.isComments = isComments self.isComments = isComments
self.count = count self.count = count
self.presentationData = presentationData 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 self.controllerInteraction = controllerInteraction
} }

View File

@ -22,7 +22,7 @@ public class ChatUnreadItem: ListViewItem {
self.index = index self.index = index
self.presentationData = presentationData self.presentationData = presentationData
self.controllerInteraction = controllerInteraction 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {

View File

@ -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 { _, _, _, _, _ in
return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _, _, _ 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 attachedAvatarNodeOffset: CGFloat = 0.0
private var attachedAvatarNodeIsHidden: Bool = false private var attachedAvatarNodeIsHidden: Bool = false
private var attachedDateHeader: (hasDate: Bool, hasPeer: Bool) = (false, false)
override open func attachedHeaderNodesUpdated() { override open func attachedHeaderNodesUpdated() {
if !self.attachedAvatarNodeOffset.isZero { if !self.attachedAvatarNodeOffset.isZero {
@ -919,6 +921,12 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
headerNode.updateAvatarIsHidden(isHidden: self.attachedAvatarNodeIsHidden, transition: .immediate) 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) { 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() { open func unreadMessageRangeUpdated() {
} }

View File

@ -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 displaySize = CGSize(width: 184.0, height: 184.0)
let telegramFile = self.telegramFile let telegramFile = self.telegramFile
let layoutConstants = self.layoutConstants let layoutConstants = self.layoutConstants
@ -435,7 +435,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
let currentShareButtonNode = self.shareButtonNode let currentShareButtonNode = self.shareButtonNode
let currentForwardInfo = self.appliedForwardInfo let currentForwardInfo = self.appliedForwardInfo
func continueAsyncLayout(_ weakSelf: Weak<ChatMessageStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { func continueAsyncLayout(_ weakSelf: Weak<ChatMessageStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) 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) 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 { if dateHeaderAtBottom.hasDate && dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight layoutInsets.top += layoutConstants.timestampDateAndTopicHeaderHeight
} else {
if dateHeaderAtBottom.hasDate {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
if dateHeaderAtBottom.hasTopic {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
} }
var deliveryFailedInset: CGFloat = 0.0 var deliveryFailedInset: CGFloat = 0.0
@ -964,6 +971,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.updateAccessibilityData(accessibilityData) strongSelf.updateAccessibilityData(accessibilityData)
strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic)
transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame) transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame)
strongSelf.enableSynchronousImageApply = true strongSelf.enableSynchronousImageApply = true
imageApply() imageApply()

View File

@ -734,7 +734,21 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
spoilerTextColor: messageTheme.primaryTextColor, spoilerTextColor: messageTheme.primaryTextColor,
spoilerEffectColor: messageTheme.secondaryTextColor, spoilerEffectColor: messageTheme.secondaryTextColor,
areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji, 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) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)

View File

@ -239,6 +239,7 @@ public final class ChatSideTopicsPanel: Component {
avatarNode = current avatarNode = current
} else { } else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0))
avatarNode.isUserInteractionEnabled = false
self.avatarNode = avatarNode self.avatarNode = avatarNode
self.containerButton.addSubview(avatarNode.view) 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<EnvironmentType>, transition: ComponentTransition) -> CGSize { func update(component: ChatSideTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {

View File

@ -1099,19 +1099,22 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
public let spoilerEffectColor: UIColor public let spoilerEffectColor: UIColor
public let areContentAnimationsEnabled: Bool public let areContentAnimationsEnabled: Bool
public let spoilerExpandRect: CGRect? public let spoilerExpandRect: CGRect?
public var crossfadeContents: ((UIView) -> Void)?
public init( public init(
animation: ListViewItemUpdateAnimation, animation: ListViewItemUpdateAnimation,
spoilerTextColor: UIColor, spoilerTextColor: UIColor,
spoilerEffectColor: UIColor, spoilerEffectColor: UIColor,
areContentAnimationsEnabled: Bool, areContentAnimationsEnabled: Bool,
spoilerExpandRect: CGRect? spoilerExpandRect: CGRect?,
crossfadeContents: ((UIView) -> Void)? = nil
) { ) {
self.animation = animation self.animation = animation
self.spoilerTextColor = spoilerTextColor self.spoilerTextColor = spoilerTextColor
self.spoilerEffectColor = spoilerEffectColor self.spoilerEffectColor = spoilerEffectColor
self.areContentAnimationsEnabled = areContentAnimationsEnabled self.areContentAnimationsEnabled = areContentAnimationsEnabled
self.spoilerExpandRect = spoilerExpandRect self.spoilerExpandRect = spoilerExpandRect
self.crossfadeContents = crossfadeContents
} }
} }

View File

@ -216,9 +216,14 @@ public final class InteractiveTextNodeWithEntities {
return (layout, { applyArguments in return (layout, { applyArguments in
let animation: ListViewItemUpdateAnimation = applyArguments.applyArguments.animation 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) let result = apply(applyArguments.applyArguments)
if let maybeNode = maybeNode { if let maybeNode {
maybeNode.attributedString = arguments.attributedString maybeNode.attributedString = arguments.attributedString
maybeNode.updateInteractiveContents( maybeNode.updateInteractiveContents(
@ -234,6 +239,10 @@ public final class InteractiveTextNodeWithEntities {
applyArguments: applyArguments.applyArguments applyArguments: applyArguments.applyArguments
) )
if let crossfadeSourceView {
applyArguments.applyArguments.crossfadeContents?(crossfadeSourceView)
}
return maybeNode return maybeNode
} else { } else {
let resultNode = InteractiveTextNodeWithEntities(textNode: result) let resultNode = InteractiveTextNodeWithEntities(textNode: result)

View File

@ -338,7 +338,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
headerNode.item = header headerNode.item = header
} }
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) 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 { } else {
headerNode = header.node(synchronousLoad: true) headerNode = header.node(synchronousLoad: true)
if headerNode.item !== header { if headerNode.item !== header {
@ -350,7 +350,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
strongSelf.itemHeaderNodes[id] = headerNode strongSelf.itemHeaderNodes[id] = headerNode
strongSelf.containerNode.addSubnode(headerNode) strongSelf.containerNode.addSubnode(headerNode)
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate)
} }
} }
} }

View File

@ -403,10 +403,14 @@ extension ChatControllerImpl {
} else { } else {
messageOptionsTitleInfo = .single(nil) messageOptionsTitleInfo = .single(nil)
} }
var isFirstTimeValue = true
self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo) 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 |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in
if let strongSelf = self { if let strongSelf = self {
let isFirstTime = isFirstTimeValue
isFirstTimeValue = false
var isScheduledMessages = false var isScheduledMessages = false
if case .scheduledMessages = presentationInterfaceState.subject { if case .scheduledMessages = presentationInterfaceState.subject {
isScheduledMessages = true isScheduledMessages = true
@ -446,6 +450,8 @@ extension ChatControllerImpl {
} else if let peer = peerViewMainPeer(peerView) { } else if let peer = peerViewMainPeer(peerView) {
if case .pinnedMessages = presentationInterfaceState.subject { if case .pinnedMessages = presentationInterfaceState.subject {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) 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 { } else {
strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo)
let imageOverride: AvatarNodeImageOverride? let imageOverride: AvatarNodeImageOverride?
@ -460,7 +466,7 @@ extension ChatControllerImpl {
} else { } else {
imageOverride = nil 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 { if case .standard(.previewing) = strongSelf.mode {
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false
} else { } else {
@ -767,16 +773,22 @@ extension ChatControllerImpl {
autoremoveTimeout = value?.effectiveValue autoremoveTimeout = value?.effectiveValue
} }
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData { } else if let cachedChannelData = peerView.cachedData as? CachedChannelData {
currentSendAsPeerId = cachedChannelData.sendAsPeerId if let channel = peer as? TelegramChannel, channel.isMonoForum {
if let channel = peer as? TelegramChannel, case .group = channel.info { if case let .known(value) = cachedChannelData.linkedMonoforumPeerId {
if !cachedChannelData.botInfos.isEmpty { currentSendAsPeerId = value
hasBots = true
} }
let botCommands = cachedChannelData.botInfos.reduce(into: [], { result, info in } else {
result.append(contentsOf: info.botInfo.commands) currentSendAsPeerId = cachedChannelData.sendAsPeerId
}) if let channel = peer as? TelegramChannel, case .group = channel.info {
if !botCommands.isEmpty { if !cachedChannelData.botInfos.isEmpty {
hasBotCommands = true 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 { if case let .known(value) = cachedChannelData.autoremoveTimeout {
@ -1139,22 +1151,39 @@ extension ChatControllerImpl {
savedMessagesPeerId = nil savedMessagesPeerId = nil
} }
let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int)?, NoError> let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int, presence: EnginePeer.Presence?)?, NoError>
if let savedMessagesPeerId { if let savedMessagesPeerId {
let threadPeerId = 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) 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]) savedMessagesPeer = context.account.postbox.combinedView(keys: [basicPeerKey, countViewKey])
|> map { views -> (peer: EnginePeer?, messageCount: Int)? in |> map { views -> (peer: EnginePeer?, messageCount: Int, presence: EnginePeer.Presence?)? in
let peer = ((views.views[basicPeerKey] as? BasicPeerView)?.peer).flatMap(EnginePeer.init) 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 var messageCount = 0
if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count {
messageCount += Int(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 { } else {
savedMessagesPeer = .single(nil) savedMessagesPeer = .single(nil)
} }
@ -1261,6 +1290,7 @@ extension ChatControllerImpl {
let globalPrivacySettings = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()) let globalPrivacySettings = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy())
self.titleDisposable.set(nil) self.titleDisposable.set(nil)
var isFirstTimeValue = true
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(),
peerView, peerView,
messageAndTopic, messageAndTopic,
@ -1275,6 +1305,9 @@ extension ChatControllerImpl {
) )
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, globalPrivacySettings in |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, globalPrivacySettings in
if let strongSelf = self { if let strongSelf = self {
let isFirstTime = isFirstTimeValue
isFirstTimeValue = false
strongSelf.hasScheduledMessages = hasScheduledMessages strongSelf.hasScheduledMessages = hasScheduledMessages
var renderedPeer: RenderedPeer? var renderedPeer: RenderedPeer?
@ -1334,16 +1367,29 @@ extension ChatControllerImpl {
} }
if let savedMessagesPeerId { if let savedMessagesPeerId {
var peerPresences: [PeerId: PeerPresence] = [:]
if let presence = savedMessagesPeer?.presence {
peerPresences[savedMessagesPeerId] = presence._asPresence()
}
let mappedPeerData = ChatTitleContent.PeerData( let mappedPeerData = ChatTitleContent.PeerData(
peerId: savedMessagesPeerId, peerId: savedMessagesPeerId,
peer: savedMessagesPeer?.peer?._asPeer(), peer: savedMessagesPeer?.peer?._asPeer(),
isContact: true, isContact: true,
isSavedMessages: true, isSavedMessages: true,
notificationSettings: nil, notificationSettings: nil,
peerPresences: [:], peerPresences: peerPresences,
cachedData: nil 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 strongSelf.peerView = peerView
@ -1374,7 +1420,7 @@ extension ChatControllerImpl {
.updatedHasScheduledMessages(hasScheduledMessages) .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.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false
strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile
} else { } else {
@ -1464,12 +1510,18 @@ extension ChatControllerImpl {
var peerGeoLocation: PeerGeoLocation? var peerGeoLocation: PeerGeoLocation?
var currentSendAsPeerId: PeerId? var currentSendAsPeerId: PeerId?
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
currentSendAsPeerId = cachedData.sendAsPeerId if peer.isMonoForum {
if case .group = peer.info { if case let .known(value) = cachedData.linkedMonoforumPeerId {
peerGeoLocation = cachedData.peerGeoLocation currentSendAsPeerId = value
} }
if case let .known(value) = cachedData.linkedDiscussionPeerId { } else {
peerDiscussionId = value 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) self.chatTitleView?.titleContent = .custom(" ", nil, false)
} }
var isFirstTimeValue = true
self.peerDisposable.set((peerView self.peerDisposable.set((peerView
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView in |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in
guard let self else { guard let self else {
return return
} }
let isFirstTime = isFirstTimeValue
isFirstTimeValue = false
var renderedPeer: RenderedPeer? var renderedPeer: RenderedPeer?
if let peerView, let peer = peerView.peers[peerView.peerId] { 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) 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 self.peerView = peerView
@ -2327,6 +2383,10 @@ extension ChatControllerImpl {
return return
} }
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.isMonoForum {
return
}
let isPremium = strongSelf.presentationInterfaceState.isPremium let isPremium = strongSelf.presentationInterfaceState.isPremium
var allPeers: [SendAsPeer]? var allPeers: [SendAsPeer]?

View File

@ -23,6 +23,8 @@ func updateChatPresentationInterfaceStateImpl(
_ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState,
completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void
) { ) {
let previousChatLocation = selfController.chatDisplayNode.historyNode.chatLocation
var completion = externalCompletion var completion = externalCompletion
var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState) var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState)
@ -434,6 +436,11 @@ func updateChatPresentationInterfaceStateImpl(
selfController.presentationInterfaceState = updatedChatPresentationInterfaceState 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() selfController.updateSlowmodeStatus()
switch updatedChatPresentationInterfaceState.inputMode { switch updatedChatPresentationInterfaceState.inputMode {
@ -489,19 +496,25 @@ func updateChatPresentationInterfaceStateImpl(
selfController.leftNavigationButton = nil selfController.leftNavigationButton = nil
} }
var buttonsAnimated = transition.isAnimated if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum {
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) { } else {
if selfController.rightNavigationButton != button { var buttonsAnimated = transition.isAnimated
if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { 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) {
buttonsAnimated = false 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 { } else if let _ = selfController.rightNavigationButton {
buttonsAnimated = false selfController.rightNavigationButton = nil
}
selfController.rightNavigationButton = button
} }
} 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) { 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.chatLocation = selfController.presentationInterfaceState.chatLocation
selfController.chatDisplayNode.chatLocation = selfController.presentationInterfaceState.chatLocation
selfController.reloadChatLocation() selfController.reloadChatLocation()
selfController.reloadCachedData() selfController.reloadCachedData()
selfController.chatDisplayNode.historyNode.updateChatLocation(chatLocation: selfController.presentationInterfaceState.chatLocation)
} }
selfController.updateDownButtonVisibility() selfController.updateDownButtonVisibility()

View File

@ -842,7 +842,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return true return true
case let .quickReplyMessageInput(_, shortcutType): case let .quickReplyMessageInput(_, shortcutType):
if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty { if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty {
let titleString: String let titleString: String
let textString: String let textString: String
switch shortcutType { switch shortcutType {
@ -7919,6 +7918,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) 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 let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId {
if attributes.first(where: { $0 is SendAsMessageAttribute }) == nil { if attributes.first(where: { $0 is SendAsMessageAttribute }) == nil {
attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId)) attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId))

View File

@ -135,7 +135,8 @@ class HistoryNodeContainer: ASDisplayNode {
class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let context: AccountContext let context: AccountContext
var chatLocation: ChatLocation private(set) var chatLocation: ChatLocation
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
let controllerInteraction: ChatControllerInteraction let controllerInteraction: ChatControllerInteraction
private weak var controller: ChatControllerImpl? private weak var controller: ChatControllerImpl?
@ -158,7 +159,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let contentContainerNode: ChatNodeContainer let contentContainerNode: ChatNodeContainer
let contentDimNode: ASDisplayNode let contentDimNode: ASDisplayNode
let backgroundNode: WallpaperBackgroundNode let backgroundNode: WallpaperBackgroundNode
let historyNode: ChatHistoryListNodeImpl var historyNode: ChatHistoryListNodeImpl
var blurredHistoryNode: ASImageNode? var blurredHistoryNode: ASImageNode?
let historyNodeContainer: HistoryNodeContainer let historyNodeContainer: HistoryNodeContainer
let loadingNode: ChatLoadingNode let loadingNode: ChatLoadingNode
@ -183,6 +184,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
private(set) var validLayout: (ContainerViewLayout, CGFloat)? private(set) var validLayout: (ContainerViewLayout, CGFloat)?
private var visibleAreaInset = UIEdgeInsets() private var visibleAreaInset = UIEdgeInsets()
private var currentListViewLayout: (size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets)?
private(set) var searchNavigationNode: ChatSearchNavigationContentNode? private(set) var searchNavigationNode: ChatSearchNavigationContentNode?
@ -293,7 +295,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
private var dropDimNode: ASDisplayNode? private var dropDimNode: ASDisplayNode?
let messageTransitionNode: ChatMessageTransitionNodeImpl var messageTransitionNode: ChatMessageTransitionNodeImpl
private let presentationContextMarker = ASDisplayNode() private let presentationContextMarker = ASDisplayNode()
@ -405,6 +407,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, statusBar: StatusBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?) { init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, statusBar: StatusBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?) {
self.context = context self.context = context
self.chatLocation = chatLocation self.chatLocation = chatLocation
self.chatLocationContextHolder = chatLocationContextHolder
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.chatPresentationInterfaceState = chatPresentationInterfaceState self.chatPresentationInterfaceState = chatPresentationInterfaceState
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
@ -762,34 +765,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in self.setupHistoryNode()
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.interactiveEmojisDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) self.interactiveEmojisDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { preferencesView -> InteractiveEmojiConfiguration in |> map { preferencesView -> InteractiveEmojiConfiguration in
@ -801,31 +777,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
strongSelf.interactiveEmojis = emojis 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.addSubnode(self.wrappingNode)
self.wrappingNode.contentNode.addSubnode(self.contentContainerNode) self.wrappingNode.contentNode.addSubnode(self.contentContainerNode)
@ -866,8 +817,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) 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.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
self?.interfaceInteraction?.presentController(controller, nil) self?.interfaceInteraction?.presentController(controller, nil)
}) })
@ -1014,44 +963,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return false 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<Bool, NoError> 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) { 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 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 { if let strongSelf = self {
strongSelf.notifyTransitionCompletionListeners(transition: transition) 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)) 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<Bool, NoError> 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()
}
}
} }

View File

@ -1233,14 +1233,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false) 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>) { private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) {
self.adMessagesDisposable = (adMessages self.adMessagesDisposable = (adMessages
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in |> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in
@ -2362,7 +2354,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
strongSelf.dynamicBounceEnabled = false strongSelf.dynamicBounceEnabled = false
strongSelf.forEachItemHeaderNode { itemHeaderNode in strongSelf.forEachItemHeaderNode { itemHeaderNode in
if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNodeImpl {
dateNode.updatePresentationData(chatPresentationData, context: strongSelf.context) dateNode.updatePresentationData(chatPresentationData, context: strongSelf.context)
} else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNodeImpl { } else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNodeImpl {
avatarNode.updatePresentationData(chatPresentationData, context: strongSelf.context) avatarNode.updatePresentationData(chatPresentationData, context: strongSelf.context)

View File

@ -231,15 +231,16 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
if channel.flags.contains(.isMonoforum) { if channel.flags.contains(.isMonoforum) {
if channel.adminRights != nil, case .peer = chatPresentationInterfaceState.chatLocation { if channel.adminRights != nil, case .peer = chatPresentationInterfaceState.chatLocation {
displayInputTextPanel = false if chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil {
displayInputTextPanel = false
if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) {
return (currentPanel, nil) return (currentPanel, nil)
} else { } else {
let panel = ChatRestrictedInputPanelNode() let panel = ChatRestrictedInputPanelNode()
panel.context = context panel.context = context
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return (panel, nil) return (panel, nil)
}
} }
} else { } else {
displayInputTextPanel = true displayInputTextPanel = true

View File

@ -90,8 +90,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
} }
if let channel = interfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, channel.isMonoForum { if let channel = interfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, channel.isMonoForum {
//TODO:localize self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelForumModeReplyText, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
self.textNode.attributedText = NSAttributedString(string: "Choose a thread to reply", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
} else if let _ = accountFreezeConfiguration?.freezeUntilDate { } 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.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) self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelFrozenAccount_Text, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)

View File

@ -868,4 +868,16 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0)) 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
}
}
} }

View File

@ -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 { 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 { 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 {

View File

@ -297,7 +297,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
headerNode.item = header headerNode.item = header
} }
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) 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 { } else {
headerNode = header.node(synchronousLoad: true) headerNode = header.node(synchronousLoad: true)
if headerNode.item !== header { if headerNode.item !== header {
@ -309,7 +309,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
strongSelf.itemHeaderNodes[id] = headerNode strongSelf.itemHeaderNodes[id] = headerNode
strongSelf.containerNode.addSubnode(headerNode) strongSelf.containerNode.addSubnode(headerNode)
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate)
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"app": "11.11", "app": "11.11",
"xcode": "16.2", "xcode": "16.3",
"bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3", "bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3",
"macos": "15" "macos": "15"
} }