@@ -10,6 +10,7 @@ class ReactionsFlowView: UIView {
10
10
private var outgoing : Bool = false
11
11
12
12
private var containerStackView : UIStackView !
13
+ private var reactionViews = [ String: MessageReactionView] ( )
13
14
14
15
var onReactionTap : ( ( String ) -> Void ) ?
15
16
@@ -49,64 +50,138 @@ class ReactionsFlowView: UIView {
49
50
50
51
// MARK: - Public Methods
51
52
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) )
68
79
}
69
80
70
- return view
81
+ // Remove from dictionary
82
+ reactionViews. removeValue ( forKey: emoji)
71
83
}
72
- print ( " REACTION VIEWS COUNT \( reactionViews. count) " )
73
84
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
+ }
79
108
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
+ }
85
116
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
+ }
88
136
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 )
91
141
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
+ }
95
157
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 {
96
172
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!)
100
177
currentRowWidth = 0
101
178
}
102
179
103
- currentRow. addArrangedSubview ( view)
180
+ currentRow! . addArrangedSubview ( view)
104
181
currentRowWidth += viewWidth + horizontalSpacing
105
182
}
106
183
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 ( )
111
186
}
112
187
}
0 commit comments