@@ -2,6 +2,12 @@ import InlineKit
2
2
import SwiftUI
3
3
import UIKit
4
4
5
+ struct Reaction2 : Equatable {
6
+ let emoji : String
7
+ var count : Int
8
+ var hasReacted : Bool
9
+ }
10
+
5
11
class UIMessageView : UIView {
6
12
// MARK: - Properties
7
13
@@ -40,19 +46,26 @@ class UIMessageView: UIView {
40
46
return stack
41
47
} ( )
42
48
49
+ private var reactions : [ Reaction2 ] = [ ]
50
+ private let reactionHeight : CGFloat = 24
51
+
52
+ private lazy var reactionsContainer : UIStackView = {
53
+ let stack = UIStackView ( )
54
+ stack. axis = . horizontal
55
+ stack. spacing = 4
56
+ stack. alignment = . center
57
+ stack. distribution = . fillProportionally
58
+ stack. translatesAutoresizingMaskIntoConstraints = false
59
+ return stack
60
+ } ( )
61
+
43
62
private var leadingConstraint : NSLayoutConstraint ?
44
63
private var trailingConstraint : NSLayoutConstraint ?
45
64
private var fullMessage : FullMessage
46
65
47
66
private let horizontalPadding : CGFloat = 12
48
67
private let verticalPadding : CGFloat = 8
49
68
50
- // private let embedView: MessageEmbedView = {
51
- // let view = MessageEmbedView(repliedToMessage: nil)
52
- // view.translatesAutoresizingMaskIntoConstraints = false
53
- // return view
54
- // }()
55
-
56
69
// MARK: - Initialization
57
70
58
71
init ( fullMessage: FullMessage ) {
@@ -76,12 +89,6 @@ class UIMessageView: UIView {
76
89
77
90
bubbleView. addSubview ( contentStack)
78
91
79
- // Add embed view if there's a reply
80
- // if let replyMessage = fullMessage.repliedToMessage {
81
- // contentStack.addArrangedSubview(embedView)
82
- // embedView.repliedToMessage = replyMessage
83
- // }
84
-
85
92
leadingConstraint = bubbleView. leadingAnchor. constraint ( equalTo: leadingAnchor, constant: 8 )
86
93
trailingConstraint = bubbleView. trailingAnchor. constraint ( equalTo: trailingAnchor, constant: - 8 )
87
94
@@ -112,12 +119,6 @@ class UIMessageView: UIView {
112
119
contentStack. arrangedSubviews. forEach { $0. removeFromSuperview ( ) }
113
120
shortMessageStack. arrangedSubviews. forEach { $0. removeFromSuperview ( ) }
114
121
115
- // Add embed view first if there's a reply
116
- // if let replyMessage = fullMessage.repliedToMessage {
117
- // contentStack.addArrangedSubview(embedView)
118
- // embedView.repliedToMessage = replyMessage
119
- // }
120
-
121
122
let messageLength = fullMessage. message. text? . count ?? 0
122
123
let messageText = fullMessage. message. text ?? " "
123
124
let hasLineBreak = messageText. contains ( " \n " )
@@ -147,10 +148,16 @@ class UIMessageView: UIView {
147
148
shortMessageStack. addArrangedSubview ( metadataView)
148
149
contentStack. addArrangedSubview ( shortMessageStack)
149
150
}
151
+
152
+ // Add reactions container if there are reactions
153
+ if !reactions. isEmpty {
154
+ contentStack. addArrangedSubview ( reactionsContainer)
155
+ }
150
156
}
151
157
152
158
private func configureForMessage( ) {
153
- messageLabel. text = fullMessage. message. text
159
+ messageLabel. text =
160
+ " \( fullMessage. message. text) \( fullMessage. message. messageId) \( fullMessage. message. chatId) "
154
161
155
162
if fullMessage. message. out == true {
156
163
bubbleView. backgroundColor = ColorManager . shared. selectedColor
@@ -188,15 +195,77 @@ class UIMessageView: UIView {
188
195
super. layoutSubviews ( )
189
196
}
190
197
191
- // override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
192
- // super.traitCollectionDidChange(previousTraitCollection)
193
- //
194
- // if previousTraitCollection?.preferredContentSizeCategory
195
- // != traitCollection.preferredContentSizeCategory
196
- // {
197
- // setNeedsLayout()
198
- // }
199
- // }
198
+ // MARK: - Reaction2 Handling
199
+
200
+ private func handleReaction2( _ emoji: String ) {
201
+ if let index = reactions. firstIndex ( where: { $0. emoji == emoji } ) {
202
+ // Toggle existing reaction
203
+ var reaction = reactions [ index]
204
+ reaction. hasReacted. toggle ( )
205
+ reaction. count += reaction. hasReacted ? 1 : - 1
206
+
207
+ if reaction. count > 0 {
208
+ reactions [ index] = reaction
209
+ } else {
210
+ reactions. remove ( at: index)
211
+ }
212
+ } else {
213
+ // Add new reaction
214
+ reactions. append ( Reaction2 ( emoji: emoji, count: 1 , hasReacted: true ) )
215
+ }
216
+
217
+ updateReaction2Views ( )
218
+ }
219
+
220
+ private func updateReaction2Views( ) {
221
+ // Clear existing reaction views
222
+ reactionsContainer. arrangedSubviews. forEach { $0. removeFromSuperview ( ) }
223
+
224
+ // Add reaction bubbles
225
+ for reaction in reactions {
226
+ let reactionView = createReaction2Bubble ( for: reaction)
227
+ reactionsContainer. addArrangedSubview ( reactionView)
228
+ }
229
+
230
+ updateMetadataLayout ( )
231
+ }
232
+
233
+ private func createReaction2Bubble( for reaction: Reaction2 ) -> UIView {
234
+ let container = UIView ( )
235
+ container. backgroundColor =
236
+ reaction. hasReacted ? ColorManager . shared. selectedColor. withAlphaComponent ( 0.1 ) : . systemGray6
237
+ container. layer. cornerRadius = reactionHeight / 2
238
+
239
+ let label = UILabel ( )
240
+ label. text = " \( reaction. emoji) \( reaction. count) "
241
+ label. font = . systemFont( ofSize: 12 )
242
+ label. textAlignment = . center
243
+
244
+ container. addSubview ( label)
245
+ label. translatesAutoresizingMaskIntoConstraints = false
246
+
247
+ NSLayoutConstraint . activate ( [
248
+ container. heightAnchor. constraint ( equalToConstant: reactionHeight) ,
249
+ label. leadingAnchor. constraint ( equalTo: container. leadingAnchor, constant: 8 ) ,
250
+ label. trailingAnchor. constraint ( equalTo: container. trailingAnchor, constant: - 8 ) ,
251
+ label. centerYAnchor. constraint ( equalTo: container. centerYAnchor) ,
252
+ ] )
253
+
254
+ // Add tap gesture
255
+ let tapGesture = UITapGestureRecognizer ( target: self , action: #selector( reactionTapped ( _: ) ) )
256
+ container. addGestureRecognizer ( tapGesture)
257
+ container. tag = reactions. firstIndex ( where: { $0. emoji == reaction. emoji } ) ?? 0
258
+
259
+ return container
260
+ }
261
+
262
+ @objc private func reactionTapped( _ gesture: UITapGestureRecognizer ) {
263
+ guard let view = gesture. view,
264
+ reactions. indices. contains ( view. tag)
265
+ else { return }
266
+
267
+ handleReaction2 ( reactions [ view. tag] . emoji)
268
+ }
200
269
}
201
270
202
271
// MARK: - Context Menu
@@ -206,18 +275,35 @@ extension UIMessageView: UIContextMenuInteractionDelegate {
206
275
_ interaction: UIContextMenuInteraction ,
207
276
configurationForMenuAtLocation location: CGPoint
208
277
) -> UIContextMenuConfiguration ? {
209
- return UIContextMenuConfiguration ( identifier: nil , previewProvider: nil ) { _ in
278
+ return UIContextMenuConfiguration ( identifier: nil , previewProvider: nil ) { [ weak self ] _ in
210
279
let copyAction = UIAction ( title: " Copy " ) { [ weak self] _ in
211
280
UIPasteboard . general. string = self ? . fullMessage. message. text
212
281
}
213
282
214
283
let replyAction = UIAction ( title: " Reply " ) { [ weak self] _ in
215
284
ChatState . shared. setReplyingMessageId (
216
- chatId: self ? . fullMessage. message. chatId ?? 0 , id: self ? . fullMessage. message. id ?? 0
285
+ chatId: self ? . fullMessage. message. chatId ?? 0 ,
286
+ id: self ? . fullMessage. message. id ?? 0
217
287
)
218
288
}
219
289
220
- return UIMenu ( children: [ copyAction, replyAction] )
290
+ // Create reaction submenu
291
+ let reactionActions = self ? . createReaction2Actions ( ) ?? [ ]
292
+ let reactMenu = UIMenu (
293
+ title: " React " , image: UIImage ( systemName: " face.smiling " ) , children: reactionActions
294
+ )
295
+
296
+ return UIMenu ( children: [ copyAction, replyAction, reactMenu] )
297
+ }
298
+ }
299
+
300
+ private func createReaction2Actions( ) -> [ UIAction ] {
301
+ let commonEmojis = [ " 👍 " , " ❤️ " , " 😂 " , " 🎉 " , " 🤔 " , " 👀 " ]
302
+
303
+ return commonEmojis. map { emoji in
304
+ UIAction ( title: emoji) { [ weak self] _ in
305
+ self ? . handleReaction2 ( emoji)
306
+ }
221
307
}
222
308
}
223
309
}
0 commit comments