6
6
//
7
7
8
8
import SwiftUI
9
+ import Combine
9
10
10
11
/**
11
12
The view that lists message objects.
@@ -18,7 +19,7 @@ import SwiftUI
18
19
19
20
- **NOTE:** The order of the messages: sending → failed → sent → delivered → seen
20
21
*/
21
- public struct MessageList < MessageType: MessageProtocol & Identifiable , RowContent: View > : View {
22
+ public struct MessageList < MessageType: MessageProtocol & Identifiable , RowContent: View , MenuContent : View > : View {
22
23
23
24
@EnvironmentObject var configuration : ChatConfiguration
24
25
@@ -27,12 +28,17 @@ public struct MessageList<MessageType: MessageProtocol & Identifiable, RowConten
27
28
@State private var isKeyboardShown = false
28
29
@State private var scrollOffset : CGFloat = 0
29
30
@State private var showsScrollButton : Bool = false
31
+ @State private var highlightMessage : MessageType ? = nil
32
+ @State private var isMessageMenuPresented : Bool = false
30
33
31
34
/// The latest message goes very first.
32
35
let showsDate : Bool
33
36
let rowContent : ( _ message: MessageType ) -> RowContent
34
37
let listName = " name.list.message "
35
38
39
+ let messageMenuContent : ( ( _ message: MessageType ) -> MenuContent ) ?
40
+ let reactionItems : [ String ]
41
+
36
42
let sendingMessages : [ MessageType ]
37
43
let failedMessages : [ MessageType ]
38
44
let sentMessages : [ MessageType ]
@@ -56,24 +62,36 @@ public struct MessageList<MessageType: MessageProtocol & Identifiable, RowConten
56
62
LazyVStack ( spacing: 0 ) {
57
63
ForEach ( sendingMessages) { message in
58
64
rowContent ( message)
65
+ . anchorPreference ( key: BoundsPreferenceKey . self, value: . bounds) { anchor in
66
+ [ message. id: anchor]
67
+ }
59
68
. padding ( . horizontal, 12 )
60
69
. effect ( . flipped)
61
70
}
62
71
63
72
ForEach ( failedMessages) { message in
64
73
rowContent ( message)
74
+ . anchorPreference ( key: BoundsPreferenceKey . self, value: . bounds) { anchor in
75
+ [ message. id: anchor]
76
+ }
65
77
. padding ( . horizontal, 12 )
66
78
. effect ( . flipped)
67
79
}
68
80
69
81
ForEach ( sentMessages) { message in
70
82
rowContent ( message)
83
+ . anchorPreference ( key: BoundsPreferenceKey . self, value: . bounds) { anchor in
84
+ [ message. id: anchor]
85
+ }
71
86
. padding ( . horizontal, 12 )
72
87
. effect ( . flipped)
73
88
}
74
89
75
90
ForEach ( deliveredMessages) { message in
76
91
rowContent ( message)
92
+ . anchorPreference ( key: BoundsPreferenceKey . self, value: . bounds) { anchor in
93
+ [ message. id: anchor]
94
+ }
77
95
. padding ( . horizontal, 12 )
78
96
. effect ( . flipped)
79
97
}
@@ -93,6 +111,9 @@ public struct MessageList<MessageType: MessageProtocol & Identifiable, RowConten
93
111
. padding ( . trailing, 21 )
94
112
}
95
113
}
114
+ . anchorPreference ( key: BoundsPreferenceKey . self, value: . bounds) { anchor in
115
+ [ message. id: anchor]
116
+ }
96
117
. padding ( . horizontal, 12 )
97
118
. effect ( . flipped)
98
119
}
@@ -149,12 +170,117 @@ public struct MessageList<MessageType: MessageProtocol & Identifiable, RowConten
149
170
. padding ( . bottom, 8 )
150
171
}
151
172
}
173
+ . overlay {
174
+ // MARK: blur
175
+ if highlightMessage != nil {
176
+ Rectangle ( )
177
+ . fill ( . ultraThinMaterial)
178
+ . ignoresSafeArea ( )
179
+ . onTapGesture {
180
+ withAnimation {
181
+ isMessageMenuPresented = false
182
+ highlightMessage = nil
183
+ }
184
+ }
185
+ }
186
+ }
187
+ . overlayPreferenceValue ( BoundsPreferenceKey . self) { values in
188
+ // MARK: Detects which message row is tapped
189
+ if let highlightMessage = highlightMessage, let preference = values. first ( where: { item in
190
+ item. key == highlightMessage. id
191
+ } ) {
192
+ GeometryReader { proxy in
193
+ let rect = proxy [ preference. value]
194
+ // presenting view as an overlay, so that it will look like custom context
195
+ VStack ( alignment: configuration. userID == highlightMessage. sender. id ? . trailing : . leading, spacing: 0 ) {
196
+ // MARK: Message Reaction
197
+ if highlightMessage. readReceipt == . seen {
198
+ ReactionSelector ( isPresented: $isMessageMenuPresented, message: highlightMessage, items: reactionItems) { reactionItem in
199
+ // MARK: Close Highlight
200
+ withAnimation ( . easeInOut) {
201
+ isMessageMenuPresented = false
202
+ self . highlightMessage = nil
203
+ }
204
+
205
+ // Reaction Publisher
206
+ withAnimation ( . easeInOut. delay ( 0.3 ) ) {
207
+ let _ = Empty < Void , Never > ( )
208
+ . sink { _ in
209
+ messageReactionPublisher. send ( ( reactionItem, highlightMessage. id) )
210
+ } receiveValue: { _ in }
211
+ }
212
+ }
213
+ . padding ( . top, rect. minY > 0 ? rect. minY - 36.5 : 0 )
214
+ } else {
215
+ Spacer ( )
216
+ . frame ( maxHeight: rect. minY > 0 ? rect. minY : 0 )
217
+ }
218
+
219
+ // MARK: Message Row
220
+ rowContent ( highlightMessage)
221
+ . frame ( width: rect. width, height: rect. height)
222
+
223
+ // MARK: Message Menu
224
+ if let messageMenuContent = messageMenuContent {
225
+ messageMenuContent ( highlightMessage)
226
+ }
227
+
228
+ Spacer ( )
229
+ }
230
+ . id ( highlightMessage. id)
231
+ . offset ( x: rect. minX)
232
+ }
233
+ . transition ( . asymmetric( insertion: . identity, removal: . offset( x: 1 ) ) )
234
+ }
235
+ }
236
+ . onReceive ( highlightMessagePublisher) { highlightMessage in
237
+ withAnimation ( . easeInOut) {
238
+ self . highlightMessage = highlightMessage as? MessageType
239
+ self . isMessageMenuPresented = true
240
+ }
241
+ }
152
242
}
153
243
244
+ /// The view that lists `messageData` which is an array of the objects that conform to ``MessageProtocol``
245
+ ///
246
+ /// - Parameters:
247
+ /// - messageData: The array of objects that conform to ``MessageProtocol``
248
+ /// - showsDate: The boolean value that indicates whether shows date or not.
249
+ /// - reactionItems: The array of reaction item that is type of `String`. e.g., `["❤️", "👍", "👎", "😆", "🎉"]`
250
+ /// - rowContent: The row content for the message list. Each row represent one `message`. It's recommended that uses ``MessageRow``.
251
+ /// - menuContent: The menu content for `message`. It's recommended that uses ``MessageMenu`` and ``MessageMenuButtonStyle``
252
+ ///
253
+ /// Example usage:
254
+ /// ```swift
255
+ /// MessageList(messages, reactionItems: ["❤️", "👍", "👎", "😆", "🎉"]) { message in
256
+ /// MessageRow(message: message)
257
+ /// } menuContent: { highlightMessage in
258
+ /// MessageMenu {
259
+ /// Button("Copy", action: copy)
260
+ /// .buttonStyle(MessageMenuButtonStyle(symbol: "doc.on.doc"))
261
+ ///
262
+ /// Divider()
263
+ ///
264
+ /// Button("Reply", action: reply)
265
+ /// .buttonStyle(MessageMenuButtonStyle(symbol: "arrowshape.turn.up.right"))
266
+ ///
267
+ /// Divider()
268
+ ///
269
+ /// Button("Delete", action: delete)
270
+ /// .buttonStyle(MessageMenuButtonStyle(symbol: "trash"))
271
+ /// }
272
+ /// .padding(.top, 12)
273
+ /// }
274
+ /// .onReceive(messageReactionPublisher) { (reactionItem, messageID) in
275
+ /// // handle message with reactionItem
276
+ /// }
277
+ /// ```
154
278
public init (
155
279
_ messageData: [ MessageType ] ,
156
280
showsDate: Bool = false , // TODO: Not Supported yet
157
- @ViewBuilder rowContent: @escaping ( _ message: MessageType ) -> RowContent
281
+ reactionItems: [ String ] = [ ] ,
282
+ @ViewBuilder rowContent: @escaping ( _ message: MessageType ) -> RowContent ,
283
+ @ViewBuilder menuContent: @escaping ( _ message: MessageType ) -> MenuContent
158
284
) {
159
285
var sendingMessages : [ MessageType ] = [ ]
160
286
var failedMessages : [ MessageType ] = [ ]
@@ -185,5 +311,40 @@ public struct MessageList<MessageType: MessageProtocol & Identifiable, RowConten
185
311
186
312
self . showsDate = showsDate
187
313
self . rowContent = rowContent
314
+ self . reactionItems = reactionItems
315
+ self . messageMenuContent = menuContent
316
+ }
317
+
318
+ /// The view that lists `messageData` which is an array of the objects that conform to ``MessageProtocol``
319
+ ///
320
+ /// - Parameters:
321
+ /// - messageData: The array of objects that conform to ``MessageProtocol``
322
+ /// - showsDate: The boolean value that indicates whether shows date or not.
323
+ /// - reactionItems: The array of reaction item that is type of `String`. e.g., `["❤️", "👍", "👎", "😆", "🎉"]`
324
+ /// - rowContent: The row content for the message list. Each row represent one `message`. It's recommended that uses ``MessageRow``.
325
+ ///
326
+ /// Example usage:
327
+ /// ```swift
328
+ /// MessageList(messages, reactionItems: ["❤️", "👍", "👎", "😆", "🎉"]) { message in
329
+ /// MessageRow(message: message)
330
+ /// }
331
+ /// .onReceive(messageReactionPublisher) { (reactionItem, messageID) in
332
+ /// // handle message with reactionItem
333
+ /// }
334
+ /// ```
335
+ public init (
336
+ _ messageData: [ MessageType ] ,
337
+ showsDate: Bool = false , // TODO: Not Supported yet
338
+ reactionItems: [ String ] = [ ] ,
339
+ @ViewBuilder rowContent: @escaping ( _ message: MessageType ) -> RowContent
340
+ ) where MenuContent == EmptyView {
341
+ self . init (
342
+ messageData,
343
+ showsDate: showsDate,
344
+ reactionItems: reactionItems,
345
+ rowContent: rowContent
346
+ ) { _ in
347
+ EmptyView ( )
348
+ }
188
349
}
189
350
}
0 commit comments