Skip to content

Commit a428bfe

Browse files
committed
Stabilize
1 parent 694c9b9 commit a428bfe

File tree

3 files changed

+150
-54
lines changed

3 files changed

+150
-54
lines changed

apple/InlineIOS/Chat/MessageReactionView.swift

+31-10
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import UIKit
44
class MessageReactionView: UIView {
55
// MARK: - Properties
66

7-
private let emoji: String
8-
private let count: Int
9-
private let byCurrentUser: Bool
10-
private let outgoing: Bool
7+
let emoji: String
8+
let count: Int
9+
let byCurrentUser: Bool
10+
let outgoing: Bool
1111

1212
var onTap: ((String) -> Void)?
1313

@@ -25,7 +25,7 @@ class MessageReactionView: UIView {
2525
private lazy var stackView: UIStackView = {
2626
let stack = UIStackView()
2727
stack.axis = .horizontal
28-
stack.spacing = 4
28+
stack.spacing = 0
2929
stack.alignment = .center
3030
stack.translatesAutoresizingMaskIntoConstraints = false
3131
return stack
@@ -40,7 +40,7 @@ class MessageReactionView: UIView {
4040

4141
private lazy var countLabel: UILabel = {
4242
let label = UILabel()
43-
label.font = UIFont.systemFont(ofSize: 12, weight: .medium)
43+
label.font = UIFont.systemFont(ofSize: 13)
4444
label.text = "\(count)"
4545
return label
4646
}()
@@ -73,6 +73,10 @@ class MessageReactionView: UIView {
7373
// Configure text colors
7474
countLabel.textColor = outgoing ? .white : .label
7575

76+
// Center the emoji and count labels
77+
stackView.distribution = .equalSpacing
78+
stackView.alignment = .center
79+
7680
// Add subviews
7781
addSubview(containerView)
7882
containerView.addSubview(stackView)
@@ -88,8 +92,8 @@ class MessageReactionView: UIView {
8892
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
8993

9094
stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
91-
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6),
92-
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -6),
95+
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
96+
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
9397
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
9498
])
9599

@@ -108,13 +112,30 @@ class MessageReactionView: UIView {
108112
// MARK: - Layout
109113

110114
override var intrinsicContentSize: CGSize {
111-
let stackSize = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
112-
return CGSize(width: stackSize.width + 5, height: stackSize.height + 8)
115+
let height = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + 8
116+
return CGSize(width: 48, height: height)
113117
}
114118

115119
override func sizeThatFits(_ size: CGSize) -> CGSize {
116120
intrinsicContentSize
117121
}
122+
123+
func updateCount(_ newCount: Int, animated: Bool) {
124+
guard count != newCount else { return }
125+
126+
if animated {
127+
UIView.animate(withDuration: 0.15, animations: {
128+
self.countLabel.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
129+
}) { _ in
130+
self.countLabel.text = "\(newCount)"
131+
UIView.animate(withDuration: 0.15) {
132+
self.countLabel.transform = .identity
133+
}
134+
}
135+
} else {
136+
countLabel.text = "\(newCount)"
137+
}
138+
}
118139
}
119140

120141
extension UIColor {

apple/InlineIOS/Chat/ReactionsFlowView.swift

+118-43
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class ReactionsFlowView: UIView {
1010
private var outgoing: Bool = false
1111

1212
private var containerStackView: UIStackView!
13+
private var reactionViews = [String: MessageReactionView]()
1314

1415
var onReactionTap: ((String) -> Void)?
1516

@@ -49,64 +50,138 @@ class ReactionsFlowView: UIView {
4950

5051
// MARK: - Public Methods
5152

52-
func configure(with reactions: [(emoji: String, count: Int, userIds: [Int64])]) {
53-
// Clear existing content
54-
containerStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
55-
56-
// Create reaction views
57-
let reactionViews = reactions.map { reaction -> MessageReactionView in
58-
let byCurrentUser = reaction.userIds.contains(Auth.shared.getCurrentUserId() ?? 0)
59-
let view = MessageReactionView(
60-
emoji: reaction.emoji,
61-
count: reaction.count,
62-
byCurrentUser: byCurrentUser,
63-
outgoing: outgoing
64-
)
65-
66-
view.onTap = { [weak self] emoji in
67-
self?.onReactionTap?(emoji)
53+
func configure(with reactions: [(emoji: String, count: Int, userIds: [Int64])], animatedEmoji: String? = nil) {
54+
// Create a dictionary of new reactions
55+
let newReactions = reactions.reduce(into: [String: (count: Int, userIds: [Int64])]()) {
56+
$0[$1.emoji] = ($1.count, $1.userIds)
57+
}
58+
59+
// Find reactions to remove and add
60+
let currentEmojis = Set(reactionViews.keys)
61+
let newEmojis = Set(newReactions.keys)
62+
let removedEmojis = currentEmojis.subtracting(newEmojis)
63+
let addedEmojis = newEmojis.subtracting(currentEmojis)
64+
65+
// Store views that need animation
66+
var viewsToRemove: [(view: UIView, originalFrame: CGRect)] = []
67+
var viewsToAdd: [MessageReactionView] = []
68+
69+
// Process removals - collect views to animate later
70+
for emoji in removedEmojis {
71+
guard let view = reactionViews[emoji] else { continue }
72+
73+
// Store original position for animation
74+
let originalFrame = view.convert(view.bounds, to: self)
75+
76+
// Only animate if this is the specific emoji being removed
77+
if emoji == animatedEmoji {
78+
viewsToRemove.append((view: view, originalFrame: originalFrame))
6879
}
6980

70-
return view
81+
// Remove from dictionary
82+
reactionViews.removeValue(forKey: emoji)
7183
}
72-
print("REACTION VIEWS COUNT \(reactionViews.count)")
7384

74-
// Calculate sizes
75-
let sizes = reactionViews.map { $0.sizeThatFits(CGSize(
76-
width: CGFloat.greatestFiniteMagnitude,
77-
height: CGFloat.greatestFiniteMagnitude
78-
)) }
85+
// Create new views but don't add to layout yet
86+
for reaction in reactions {
87+
if addedEmojis.contains(reaction.emoji) {
88+
let byCurrentUser = reaction.userIds.contains(Auth.shared.getCurrentUserId() ?? 0)
89+
let view = MessageReactionView(
90+
emoji: reaction.emoji,
91+
count: reaction.count,
92+
byCurrentUser: byCurrentUser,
93+
outgoing: outgoing
94+
)
95+
96+
view.onTap = { [weak self] emoji in
97+
self?.onReactionTap?(emoji)
98+
}
99+
100+
reactionViews[reaction.emoji] = view
101+
102+
// Only animate if this is the specific emoji being added
103+
if reaction.emoji == animatedEmoji {
104+
viewsToAdd.append(view)
105+
}
106+
}
107+
}
79108

80-
// Organize into rows
81-
var currentRow = UIStackView()
82-
currentRow.axis = .horizontal
83-
currentRow.spacing = horizontalSpacing
84-
currentRow.alignment = .center
109+
// Update existing reactions
110+
for (emoji, view) in reactionViews {
111+
if let newCount = newReactions[emoji]?.count, newCount != view.count {
112+
// Animate count change only for the specific emoji
113+
view.updateCount(newCount, animated: emoji == animatedEmoji)
114+
}
115+
}
85116

86-
var currentRowWidth: CGFloat = 0
87-
let maxWidth = UIScreen.main.bounds.width * 0.7 // Adjust as needed
117+
// Disable animations temporarily for layout rebuild
118+
UIView.performWithoutAnimation {
119+
// Clear and rebuild the entire layout
120+
rebuildLayout(with: Array(reactionViews.values))
121+
}
122+
123+
// Now animate removals using snapshots
124+
for (view, originalFrame) in viewsToRemove {
125+
let snapshot = view.snapshotView(afterScreenUpdates: true) ?? UIView()
126+
snapshot.frame = originalFrame
127+
addSubview(snapshot)
128+
129+
UIView.animate(withDuration: 0.2, animations: {
130+
snapshot.alpha = 0
131+
snapshot.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
132+
}) { _ in
133+
snapshot.removeFromSuperview()
134+
}
135+
}
88136

89-
for (index, view) in reactionViews.enumerated() {
90-
let viewWidth = sizes[index].width
137+
// Animate additions
138+
for view in viewsToAdd {
139+
view.alpha = 0
140+
view.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
91141

92-
if currentRowWidth + viewWidth > maxWidth, currentRowWidth > 0 {
93-
// Add the current row and start a new one
94-
containerStackView.addArrangedSubview(currentRow)
142+
UIView.animate(withDuration: 0.3, delay: 0.1, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5) {
143+
view.alpha = 1
144+
view.transform = .identity
145+
}
146+
}
147+
}
148+
149+
// MARK: - Private Methods
150+
151+
private func rebuildLayout(with views: [MessageReactionView]) {
152+
// Remove all existing rows
153+
for arrangedSubview in containerStackView.arrangedSubviews {
154+
containerStackView.removeArrangedSubview(arrangedSubview)
155+
arrangedSubview.removeFromSuperview()
156+
}
95157

158+
// Sort views to maintain consistent order
159+
let sortedViews = views.sorted { $0.emoji < $1.emoji }
160+
161+
var currentRow: UIStackView?
162+
var currentRowWidth: CGFloat = 0
163+
let maxWidth = UIScreen.main.bounds.width * 0.7
164+
165+
for view in sortedViews {
166+
let viewWidth = view.sizeThatFits(CGSize(
167+
width: CGFloat.greatestFiniteMagnitude,
168+
height: CGFloat.greatestFiniteMagnitude
169+
)).width
170+
171+
if currentRow == nil || currentRowWidth + viewWidth + horizontalSpacing > maxWidth {
96172
currentRow = UIStackView()
97-
currentRow.axis = .horizontal
98-
currentRow.spacing = horizontalSpacing
99-
currentRow.alignment = .center
173+
currentRow!.axis = .horizontal
174+
currentRow!.spacing = horizontalSpacing
175+
currentRow!.alignment = .center
176+
containerStackView.addArrangedSubview(currentRow!)
100177
currentRowWidth = 0
101178
}
102179

103-
currentRow.addArrangedSubview(view)
180+
currentRow!.addArrangedSubview(view)
104181
currentRowWidth += viewWidth + horizontalSpacing
105182
}
106183

107-
// Add the last row if it has any views
108-
if currentRow.arrangedSubviews.count > 0 {
109-
containerStackView.addArrangedSubview(currentRow)
110-
}
184+
// Force layout update
185+
layoutIfNeeded()
111186
}
112187
}

apple/InlineIOS/Chat/UIMessageView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ class UIMessageView: UIView {
294294
containerStack.addArrangedSubview(newPhotoView)
295295
}
296296

297-
private func setupReactionsIfNeeded() {
297+
private func setupReactionsIfNeeded(animatedEmoji: String? = nil) {
298298
print("SETTING UP REACTIONS \(fullMessage.reactions.count)")
299299
guard !fullMessage.reactions.isEmpty else { return }
300300

0 commit comments

Comments
 (0)