@@ -10,7 +10,7 @@ private enum Environment: Equatable {
1010 /// - Parameters:
1111 /// - key: Your Ably API key.
1212 /// - clientId: A string that identifies this client.
13- case live( key: String , clientId : String )
13+ case live( key: String , clientID : String )
1414
1515 @MainActor
1616 func createChatClient( ) -> any ChatClientProtocol {
@@ -19,10 +19,10 @@ private enum Environment: Equatable {
1919 return MockChatClient (
2020 clientOptions: ChatClientOptions ( ) ,
2121 )
22- case let . live( key: key, clientId : clientId ) :
22+ case let . live( key: key, clientID : clientID ) :
2323 let realtimeOptions = ARTClientOptions ( )
2424 realtimeOptions. key = key
25- realtimeOptions. clientId = clientId
25+ realtimeOptions. clientId = clientID
2626 let realtime = ARTRealtime ( options: realtimeOptions)
2727
2828 return ChatClient ( realtime: realtime, clientOptions: . init( ) )
@@ -45,6 +45,7 @@ struct ContentView: View {
4545 @State private var chatClient = Environment . current. createChatClient ( )
4646 @State private var currentClientID : String ?
4747
48+ @State private var isLoadingHistory = true
4849 @State private var reactions : [ Reaction ] = [ ]
4950 @State private var newMessage = " "
5051 @State private var typingInfo = " "
@@ -114,81 +115,102 @@ struct ContentView: View {
114115 . font ( . footnote)
115116 . frame ( height: 12 )
116117 . padding ( . horizontal, 8 )
117- List ( listItems, id: \. id) { item in
118- switch item {
119- case let . message( messageItem) :
120- if messageItem. message. action == . messageDelete {
121- DeletedMessageView ( item: messageItem)
122- . flip ( )
123- } else {
124- MessageView (
125- currentClientID: currentClientID,
126- item: messageItem,
127- isEditing: Binding ( get: {
128- editingItemID == messageItem. message. serial
129- } , set: { editing in
130- editingItemID = editing ? messageItem. message. serial : nil
131- newMessage = editing ? messageItem. message. text : " "
132- } ) ,
133- onDeleteMessage: {
134- deleteMessage ( messageItem. message)
135- } ,
136- onAddReaction: { reaction in
137- addMessageReaction ( reaction, messageSerial: messageItem. message. serial)
138- } ,
139- onDeleteReaction: { reaction in
140- deleteMessageReaction ( reaction, messageSerial: messageItem. message. serial)
141- } ,
142- ) . id ( item. id)
143- . flip ( )
118+ if isLoadingHistory {
119+ VStack ( spacing: 16 ) {
120+ ProgressView ( )
121+ Text ( " Loading messages... " )
122+ . foregroundStyle ( . secondary)
123+ }
124+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
125+ } else {
126+ // Don't show the scroll view until we've loaded history, since the defaultScrollAnchor doesn't behave well when you insert a load of messages (i.e. it doesn't remain anchored at bottom).
127+ ScrollView {
128+ // The ideal here would be to use LazyVStack, but that seems to not interact very well with the defaultScrollAnchor; sometimes (e.g. presence message display) new content arrives in the scroll view and it doesn't scroll to the bottom.
129+ //
130+ // No doubt there are performance implications of not using LazyVStack, but we can deal with that some other time.
131+ VStack ( alignment: . leading, spacing: 8 ) {
132+ ForEach ( listItems, id: \. id) { item in
133+ Group {
134+ switch item {
135+ case let . message( messageItem) :
136+ if messageItem. message. action == . messageDelete {
137+ DeletedMessageView ( item: messageItem)
138+ } else {
139+ MessageView (
140+ currentClientID: currentClientID,
141+ item: messageItem,
142+ isEditing: Binding ( get: {
143+ editingItemID == messageItem. message. serial
144+ } , set: { editing in
145+ editingItemID = editing ? messageItem. message. serial : nil
146+ newMessage = editing ? messageItem. message. text : " "
147+ } ) ,
148+ onDeleteMessage: {
149+ deleteMessage ( messageItem. message)
150+ } ,
151+ onAddReaction: { reaction in
152+ addMessageReaction ( reaction, messageSerial: messageItem. message. serial)
153+ } ,
154+ onDeleteReaction: { reaction in
155+ deleteMessageReaction ( reaction, messageSerial: messageItem. message. serial)
156+ } ,
157+ ) . id ( item. id)
158+ }
159+ case let . presence( item) :
160+ PresenceMessageView ( item: item)
161+ }
162+ }
163+ . padding ( . horizontal, 12 )
164+ }
144165 }
145- case let . presence( item) :
146- PresenceMessageView ( item: item)
147- . flip ( )
148166 }
167+ // Keep the scroll view scrolled to the bottom (unless the user manually scrolls away).
168+ . defaultScrollAnchor ( . bottom)
149169 }
150- . flip ( )
151- . listStyle ( PlainListStyle ( ) )
152- HStack {
153- TextField ( " Type a message... " , text : $ newMessage)
154- . onChange ( of : newMessage ) {
155- // this ensures that typing events are sent only when the message is actually changed whilst editing
156- if let index = listItems . firstIndex ( where : { $0 . id == editingItemID } ) {
157- if case let . message ( messageItem ) = listItems [ index ] {
158- if newMessage != messageItem . message . text {
159- startTyping ( )
170+ #if !os(tvOS )
171+ HStack {
172+ TextField ( " Type a message... " , text : $newMessage )
173+ . onChange ( of : newMessage) {
174+ // this ensures that typing events are sent only when the message is actually changed whilst editing
175+ if let index = listItems . firstIndex ( where : { $0 . id == editingItemID } ) {
176+ if case let . message ( messageItem ) = listItems [ index ] {
177+ if newMessage != messageItem . message . text {
178+ startTyping ( )
179+ }
160180 }
181+ } else {
182+ startTyping ( )
161183 }
162- } else {
163- startTyping ( )
164184 }
185+ // Send message when user presses Enter
186+ . onSubmit {
187+ sendButtonAction ( )
188+ }
189+ . textFieldStyle ( . roundedBorder)
190+ Button ( action: sendButtonAction) {
191+ #if os(iOS)
192+ Text ( sendTitle)
193+ . foregroundColor ( . white)
194+ . padding ( . vertical, 6 )
195+ . padding ( . horizontal, 12 )
196+ . background ( Color . blue)
197+ . cornerRadius ( 15 )
198+ #else
199+ Text ( sendTitle)
200+ #endif
165201 }
166- #if !os(tvOS)
167- . textFieldStyle( . roundedBorder)
168- #endif
169- Button ( action: sendButtonAction) {
170- #if os(iOS)
171- Text ( sendTitle)
172- . foregroundColor ( . white)
173- . padding ( . vertical, 6 )
174- . padding ( . horizontal, 12 )
175- . background ( Color . blue)
176- . cornerRadius ( 15 )
177- #else
178- Text ( sendTitle)
179- #endif
180- }
181- if editingItemID != nil {
182- Button ( " " , systemImage: " xmark.circle.fill " ) {
183- editingItemID = nil
184- newMessage = " "
202+ if editingItemID != nil {
203+ Button ( " " , systemImage: " xmark.circle.fill " ) {
204+ editingItemID = nil
205+ newMessage = " "
206+ }
207+ . foregroundStyle ( . red. opacity ( 0.8 ) )
208+ . transition ( . scale. combined ( with: . opacity) )
185209 }
186- . foregroundStyle ( . red. opacity ( 0.8 ) )
187- . transition ( . scale. combined ( with: . opacity) )
188210 }
189- }
190- . animation ( . easeInOut , value : editingItemID )
191- . padding ( . horizontal , 12 )
211+ . animation ( . easeInOut , value : editingItemID )
212+ . padding ( . horizontal , 12 )
213+ #endif
192214 HStack {
193215 Text ( typingInfo)
194216 . font ( . footnote)
@@ -260,14 +282,13 @@ struct ContentView: View {
260282 switch event. type {
261283 case . created:
262284 withAnimation {
263- listItems. insert (
285+ listItems. append (
264286 . message(
265287 . init(
266288 message: message,
267289 isSender: message. clientID == currentClientID,
268290 ) ,
269291 ) ,
270- at: 0 ,
271292 )
272293 }
273294 case . updated, . deleted:
@@ -288,14 +309,15 @@ struct ContentView: View {
288309 }
289310 }
290311 }
312+
291313 let previousMessages = try await subscription. historyBeforeSubscribe ( withParams: . init( ) )
314+ defer { isLoadingHistory = false }
292315
316+ // previousMessages are in newest-to-oldest order
293317 for message in previousMessages. items {
294318 switch message. action {
295319 case . messageCreate, . messageUpdate, . messageDelete:
296- withAnimation {
297- listItems. append ( . message( . init( message: message, isSender: message. clientID == currentClientID) ) )
298- }
320+ listItems. insert ( . message( . init( message: message, isSender: message. clientID == currentClientID) ) , at: 0 )
299321 }
300322 }
301323 }
@@ -332,13 +354,12 @@ struct ContentView: View {
332354 func subscribeToPresence( room: any Room ) {
333355 room. presence. subscribe { event in
334356 withAnimation {
335- listItems. insert (
357+ listItems. append (
336358 . presence(
337359 . init(
338360 presence: event,
339361 ) ,
340362 ) ,
341- at: 0 ,
342363 )
343364 }
344365 }
@@ -539,10 +560,3 @@ extension PresenceEventType {
539560 }
540561 }
541562}
542-
543- extension View {
544- func flip( ) -> some View {
545- rotationEffect ( . radians( . pi) )
546- . scaleEffect ( x: - 1 , y: 1 , anchor: . center)
547- }
548- }
0 commit comments