This commit is contained in:
Isaac 2026-04-14 15:33:19 +02:00
parent 430ea53ed8
commit 976c2de599
14 changed files with 125 additions and 26 deletions

View file

@ -67,6 +67,8 @@ private extension ComponentTransition.Animation.Curve {
switch self {
case .easeInOut:
return CAMediaTimingFunction(name: .easeInEaseOut)
case .easeIn:
return CAMediaTimingFunction(name: .easeIn)
case .linear:
return CAMediaTimingFunction(name: .linear)
case let .custom(a, b, c, d):
@ -82,6 +84,8 @@ private extension ComponentTransition.Animation.Curve {
return [.curveLinear]
case .easeInOut:
return [.curveEaseInOut]
case .easeIn:
return [.curveEaseIn]
case .spring:
return UIView.AnimationOptions(rawValue: 7 << 16)
case .custom:
@ -141,6 +145,7 @@ public struct ComponentTransition {
public enum Animation {
public enum Curve {
case easeInOut
case easeIn
case spring
case linear
case custom(Float, Float, Float, Float)
@ -150,6 +155,8 @@ public struct ComponentTransition {
switch self {
case .easeInOut:
return listViewAnimationCurveEaseInOut(offset)
case .easeIn:
return listViewAnimationCurveEaseIn(offset)
case .spring:
return listViewAnimationCurveSystem(offset)
case .linear:

View file

@ -10,6 +10,8 @@ public extension ComponentTransition.Animation.Curve {
self = .linear
case .easeInOut:
self = .easeInOut
case .easeIn:
self = .easeIn
case let .custom(a, b, c, d):
self = .custom(a, b, c, d)
case .customSpring:
@ -25,6 +27,8 @@ public extension ComponentTransition.Animation.Curve {
return .linear
case .easeInOut:
return .easeInOut
case .easeIn:
return .easeIn
case .spring:
return .spring
case let .custom(a, b, c, d):

View file

@ -12,6 +12,7 @@ extension CGRect {
public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
case linear
case easeInOut
case easeIn
case spring
case customSpring(damping: CGFloat, initialVelocity: CGFloat)
case custom(Float, Float, Float, Float)
@ -28,6 +29,8 @@ public extension ContainedViewLayoutTransitionCurve {
return offset
case .easeInOut:
return listViewAnimationCurveEaseInOut(offset)
case .easeIn:
return listViewAnimationCurveEaseIn(offset)
case .spring:
return listViewAnimationCurveSystem(offset)
case .customSpring:
@ -45,6 +48,8 @@ public extension ContainedViewLayoutTransitionCurve {
return CAMediaTimingFunctionName.linear.rawValue
case .easeInOut:
return CAMediaTimingFunctionName.easeInEaseOut.rawValue
case .easeIn:
return CAMediaTimingFunctionName.easeIn.rawValue
case .spring:
return kCAMediaTimingFunctionSpring
case let .customSpring(damping, initialVelocity):
@ -60,6 +65,8 @@ public extension ContainedViewLayoutTransitionCurve {
return nil
case .easeInOut:
return nil
case .easeIn:
return nil
case .spring:
return nil
case .customSpring:
@ -75,6 +82,8 @@ public extension ContainedViewLayoutTransitionCurve {
return [.curveLinear]
case .easeInOut:
return [.curveEaseInOut]
case .easeIn:
return [.curveEaseIn]
case .spring:
return UIView.AnimationOptions(rawValue: 7 << 16)
case .customSpring:

View file

@ -3981,6 +3981,12 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
} else {
headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
}
case .easeIn:
if transition.1 {
headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: ContainedViewLayoutTransitionCurve.slide.mediaTimingFunction)
} else {
headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn))
}
}
}

View file

@ -219,6 +219,8 @@ public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTra
return (animationDuration, .Default(duration: animationDuration))
case .easeInOut:
return (animationDuration, .Default(duration: animationDuration))
case .easeIn:
return (animationDuration, .Default(duration: animationDuration))
case .spring, .customSpring:
return (animationDuration, .Spring(duration: animationDuration))
case let .custom(c1x, c1y, c2x, c2y):

View file

@ -100,11 +100,13 @@ func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManag
for (_, index) in readMessageIndexByNamespace {
if let threadId {
var newCountIsZero = false
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if index.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: index) {
data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count))
data.maxIncomingReadId = index.id.id
newCountIsZero = data.incomingUnreadCount == 0
}
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
@ -124,6 +126,23 @@ func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManag
}
}
}
if newCountIsZero, let peer = transaction.getPeer(peerId), peer.isForumOrMonoForum {
var allTopicsAreRead = true
for item in transaction.getMessageHistoryThreadIndex(peerId: peer.id, limit: 100) {
guard let data = transaction.getMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
if data.incomingUnreadCount != 0 {
allTopicsAreRead = false
break
}
}
if allTopicsAreRead {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}
}
} else {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}

View file

@ -339,7 +339,6 @@ private class ReplyThreadHistoryContextImpl {
}
var markMainAsRead = false
markMainAsRead = !"".isEmpty
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if messageIndex.id.id >= data.maxIncomingReadId {
@ -348,6 +347,7 @@ private class ReplyThreadHistoryContextImpl {
data.maxIncomingReadId = messageIndex.id.id
}
var newCountIsZero = false
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
if messageIndex.id.id >= topMessageIndex.id.id {
let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id)
@ -357,6 +357,24 @@ private class ReplyThreadHistoryContextImpl {
}
}
}
newCountIsZero = data.incomingUnreadCount == 0
if newCountIsZero, let peer = transaction.getPeer(peerId), peer.isForumOrMonoForum {
var allTopicsAreRead = true
for item in transaction.getMessageHistoryThreadIndex(peerId: peer.id, limit: 100) {
guard let data = transaction.getMessageHistoryThreadInfo(peerId: messageIndex.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
if data.incomingUnreadCount != 0 {
allTopicsAreRead = false
break
}
}
if allTopicsAreRead {
markMainAsRead = true
}
}
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)

View file

@ -576,7 +576,7 @@ public final class AsyncListComponent: Component {
case let .curve(duration, curve):
updateSizeAndInsets.duration = duration
switch curve {
case .linear, .easeInOut:
case .linear, .easeInOut, .easeIn:
updateSizeAndInsets.curve = .Default(duration: duration)
case .spring:
updateSizeAndInsets.curve = .Spring(duration: duration)

View file

@ -252,7 +252,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} else {
currentMaxGlyphCount = nil
}
let previousGlyphCount = self.textNode.textNode.getCharacterToGlyphMapping().count
let previousGlyphCount = self.textNode.textNode.cachedLayout?.attributedString?.length ?? 0
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -711,7 +711,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
lineColor: messageTheme.accentControlColor,
displayContentsUnderSpoilers: displayContentsUnderSpoilers.value,
customTruncationToken: customTruncationToken,
expandedBlocks: expandedBlockIds
expandedBlocks: expandedBlockIds,
computeCharacterRects: true
))
var hasDraft = false
@ -729,7 +730,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
var clippedGlyphCountLayout: TextNodeLayout.LayoutInfo?
if let maxGlyphCount {
clippedGlyphCountLayout = textLayout.layoutForGlyphCount(glyphCount: maxGlyphCount)
clippedGlyphCountLayout = textLayout.layoutForCharacterCount(characterCount: maxGlyphCount)
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
@ -826,7 +827,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
var previousAnimateGlyphCount: Int?
if hasDraft || hadDraft {
previousAnimateGlyphCount = strongSelf.textNode.textNode.getCharacterToGlyphMapping().count
previousAnimateGlyphCount = strongSelf.textNode.textNode.cachedLayout?.attributedString?.length ?? 0
}
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
@ -1052,8 +1053,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hasDraft || hadDraft {
if strongSelf.textNode.textNode.revealGlyphCount == nil {
strongSelf.textNode.textNode.updateRevealGlyphCount(count: previousAnimateGlyphCount ?? 0)
if strongSelf.textNode.textNode.revealCharacterCount == nil {
strongSelf.textNode.textNode.updateRevealCharacterCount(count: previousAnimateGlyphCount ?? 0, animated: false)
}
strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0)
}
@ -1066,7 +1067,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private func updateTextRevealAnimation(previousGlyphCount: Int) {
var fromCount = previousGlyphCount
let toCount = self.textNode.textNode.getCharacterToGlyphMapping().count
let toCount = self.textNode.textNode.cachedLayout?.attributedString?.length ?? 0
let timestamp = CACurrentMediaTime()
if let textRevealAnimationState = self.textRevealAnimationState {
if textRevealAnimationState.toCount == toCount {
@ -1078,7 +1079,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if self.textRevealAnimationState != nil {
self.textRevealAnimationState = nil
self.textRevealLink = nil
self.textNode.textNode.updateRevealGlyphCount(count: nil)
self.textNode.textNode.updateRevealCharacterCount(count: nil, animated: false)
}
return
}
@ -1106,14 +1107,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.textRevealAnimationState = nil
self.textRevealLink = nil
self.textNode.textNode.updateRevealGlyphCount(count: nil)
self.textNode.textNode.updateRevealCharacterCount(count: nil, animated: false)
self.requestFullUpdate?()
} else {
var requestUpdate = false
let glyphCount = textRevealAnimationState.glyphCount(timestamp: timestamp)
if let previousRevealGlyphCount = self.textNode.textNode.revealGlyphCount, previousRevealGlyphCount != glyphCount {
if let previousRevealGlyphCount = self.textNode.textNode.revealCharacterCount, previousRevealGlyphCount != glyphCount {
if let cachedLayout = self.textNode.textNode.cachedLayout {
if cachedLayout.sizeForGlyphCount(glyphCount: previousRevealGlyphCount) != cachedLayout.sizeForGlyphCount(glyphCount: glyphCount) {
if cachedLayout.sizeForCharacterCount(characterCount: previousRevealGlyphCount) != cachedLayout.sizeForCharacterCount(characterCount: glyphCount) {
requestUpdate = true
}
} else {
@ -1123,7 +1124,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
//print("glyphCount: request update")
}
self.textNode.textNode.updateRevealGlyphCount(count: glyphCount)
self.textNode.textNode.updateRevealCharacterCount(count: glyphCount, animated: true)
}
if requestUpdate {

View file

@ -2985,15 +2985,40 @@ final class TextContentItemLayer: SimpleLayer {
self.addSublayer(snippetLayer)
self.animatingSnippetLayers.append(snippetLayer)
snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
snippetLayer.animateSpring(from: 0.001, to: 1.0, keyPath: "transform.scale", duration: 0.5, completion: { [weak self, weak snippetLayer] _ in
guard let self, let snippetLayer else {
return
}
snippetLayer.removeFromSuperlayer()
self.animatingSnippetLayers.removeAll(where: { $0 === snippetLayer })
self.updateRevealMask(animateNewSegments: false)
})
if !"".isEmpty {
//ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)).animateBlur(layer: snippetLayer, fromRadius: 10.0, toRadius: 0.0)
snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
snippetLayer.animateScale(from: 0.1, to: 1.0, duration: 0.2, completion: { [weak self, weak snippetLayer] _ in
guard let self, let snippetLayer else {
return
}
snippetLayer.removeFromSuperlayer()
self.animatingSnippetLayers.removeAll(where: { $0 === snippetLayer })
self.updateRevealMask(animateNewSegments: false)
})
} else if "".isEmpty {
ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)).animateBlur(layer: snippetLayer, fromRadius: 8.0, toRadius: 0.0)
snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
snippetLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: CGPoint(), duration: 0.2, additive: true)
snippetLayer.animateScale(from: 0.5, to: 1.0, duration: 0.2, completion: { [weak self, weak snippetLayer] _ in
guard let self, let snippetLayer else {
return
}
snippetLayer.removeFromSuperlayer()
self.animatingSnippetLayers.removeAll(where: { $0 === snippetLayer })
self.updateRevealMask(animateNewSegments: false)
})
} else {
snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self, weak snippetLayer] _ in
guard let self, let snippetLayer else {
return
}
snippetLayer.removeFromSuperlayer()
self.animatingSnippetLayers.removeAll(where: { $0 === snippetLayer })
self.updateRevealMask(animateNewSegments: false)
})
}
}
globalCharIndex += lineCharCount
}

View file

@ -504,7 +504,9 @@ final class MiniAppListScreenComponent: Component {
let timingFunction: String
switch curve {
case .easeInOut:
timingFunction = CAMediaTimingFunctionName.easeOut.rawValue
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
case .easeIn:
timingFunction = CAMediaTimingFunctionName.easeIn.rawValue
case .linear:
timingFunction = CAMediaTimingFunctionName.linear.rawValue
case .spring:

View file

@ -1064,7 +1064,9 @@ final class QuickReplySetupScreenComponent: Component {
let timingFunction: String
switch curve {
case .easeInOut:
timingFunction = CAMediaTimingFunctionName.easeOut.rawValue
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
case .easeIn:
timingFunction = CAMediaTimingFunctionName.easeIn.rawValue
case .linear:
timingFunction = CAMediaTimingFunctionName.linear.rawValue
case .spring:

View file

@ -522,7 +522,9 @@ final class PeerSelectionScreenComponent: Component {
let timingFunction: String
switch curve {
case .easeInOut:
timingFunction = CAMediaTimingFunctionName.easeOut.rawValue
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
case .easeIn:
timingFunction = CAMediaTimingFunctionName.easeIn.rawValue
case .linear:
timingFunction = CAMediaTimingFunctionName.linear.rawValue
case .spring:

View file

@ -74,6 +74,8 @@ public final class AnimatableProperty<T: Interpolatable> {
break
case .easeInOut:
t = listViewAnimationCurveEaseInOut(t)
case .easeIn:
t = listViewAnimationCurveEaseIn(t)
case .spring:
t = lookupSpringValue(t)
case let .custom(x1, y1, x2, y2):