1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2use channel::{ChannelChat, ChannelMessageId, MessageParams};
3use collab_ui::chat_panel::ChatPanel;
4use gpui::{BackgroundExecutor, Entity, TestAppContext};
5use rpc::Notification;
6use workspace::dock::Panel;
7
8#[gpui::test]
9async fn test_basic_channel_messages(
10 executor: BackgroundExecutor,
11 mut cx_a: &mut TestAppContext,
12 mut cx_b: &mut TestAppContext,
13 mut cx_c: &mut TestAppContext,
14) {
15 let mut server = TestServer::start(executor.clone()).await;
16 let client_a = server.create_client(cx_a, "user_a").await;
17 let client_b = server.create_client(cx_b, "user_b").await;
18 let client_c = server.create_client(cx_c, "user_c").await;
19
20 let channel_id = server
21 .make_channel(
22 "the-channel",
23 None,
24 (&client_a, cx_a),
25 &mut [(&client_b, cx_b), (&client_c, cx_c)],
26 )
27 .await;
28
29 let channel_chat_a = client_a
30 .channel_store()
31 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
32 .await
33 .unwrap();
34 let channel_chat_b = client_b
35 .channel_store()
36 .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
37 .await
38 .unwrap();
39
40 let message_id = channel_chat_a
41 .update(cx_a, |c, cx| {
42 c.send_message(
43 MessageParams {
44 text: "hi @user_c!".into(),
45 mentions: vec![(3..10, client_c.id())],
46 reply_to_message_id: None,
47 },
48 cx,
49 )
50 .unwrap()
51 })
52 .await
53 .unwrap();
54 channel_chat_a
55 .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
56 .await
57 .unwrap();
58
59 executor.run_until_parked();
60 channel_chat_b
61 .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
62 .await
63 .unwrap();
64
65 executor.run_until_parked();
66
67 let channel_chat_c = client_c
68 .channel_store()
69 .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
70 .await
71 .unwrap();
72
73 for (chat, cx) in [
74 (&channel_chat_a, &mut cx_a),
75 (&channel_chat_b, &mut cx_b),
76 (&channel_chat_c, &mut cx_c),
77 ] {
78 chat.update(*cx, |c, _| {
79 assert_eq!(
80 c.messages()
81 .iter()
82 .map(|m| (m.body.as_str(), m.mentions.as_slice()))
83 .collect::<Vec<_>>(),
84 vec![
85 ("hi @user_c!", [(3..10, client_c.id())].as_slice()),
86 ("two", &[]),
87 ("three", &[])
88 ],
89 "results for user {}",
90 c.client().id(),
91 );
92 });
93 }
94
95 client_c.notification_store().update(cx_c, |store, _| {
96 assert_eq!(store.notification_count(), 2);
97 assert_eq!(store.unread_notification_count(), 1);
98 assert_eq!(
99 store.notification_at(0).unwrap().notification,
100 Notification::ChannelMessageMention {
101 message_id,
102 sender_id: client_a.id(),
103 channel_id: channel_id.0,
104 }
105 );
106 assert_eq!(
107 store.notification_at(1).unwrap().notification,
108 Notification::ChannelInvitation {
109 channel_id: channel_id.0,
110 channel_name: "the-channel".to_string(),
111 inviter_id: client_a.id()
112 }
113 );
114 });
115}
116
117#[gpui::test]
118async fn test_rejoin_channel_chat(
119 executor: BackgroundExecutor,
120 cx_a: &mut TestAppContext,
121 cx_b: &mut TestAppContext,
122) {
123 let mut server = TestServer::start(executor.clone()).await;
124 let client_a = server.create_client(cx_a, "user_a").await;
125 let client_b = server.create_client(cx_b, "user_b").await;
126
127 let channel_id = server
128 .make_channel(
129 "the-channel",
130 None,
131 (&client_a, cx_a),
132 &mut [(&client_b, cx_b)],
133 )
134 .await;
135
136 let channel_chat_a = client_a
137 .channel_store()
138 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
139 .await
140 .unwrap();
141 let channel_chat_b = client_b
142 .channel_store()
143 .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
144 .await
145 .unwrap();
146
147 channel_chat_a
148 .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
149 .await
150 .unwrap();
151 channel_chat_b
152 .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap())
153 .await
154 .unwrap();
155
156 server.forbid_connections();
157 server.disconnect_client(client_a.peer_id().unwrap());
158
159 // While client A is disconnected, clients A and B both send new messages.
160 channel_chat_a
161 .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
162 .await
163 .unwrap_err();
164 channel_chat_a
165 .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
166 .await
167 .unwrap_err();
168 channel_chat_b
169 .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap())
170 .await
171 .unwrap();
172 channel_chat_b
173 .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap())
174 .await
175 .unwrap();
176
177 // Client A reconnects.
178 server.allow_connections();
179 executor.advance_clock(RECONNECT_TIMEOUT);
180
181 // Client A fetches the messages that were sent while they were disconnected
182 // and resends their own messages which failed to send.
183 let expected_messages = &["one", "two", "five", "six", "three", "four"];
184 assert_messages(&channel_chat_a, expected_messages, cx_a);
185 assert_messages(&channel_chat_b, expected_messages, cx_b);
186}
187
188#[gpui::test]
189async fn test_remove_channel_message(
190 executor: BackgroundExecutor,
191 cx_a: &mut TestAppContext,
192 cx_b: &mut TestAppContext,
193 cx_c: &mut TestAppContext,
194) {
195 let mut server = TestServer::start(executor.clone()).await;
196 let client_a = server.create_client(cx_a, "user_a").await;
197 let client_b = server.create_client(cx_b, "user_b").await;
198 let client_c = server.create_client(cx_c, "user_c").await;
199
200 let channel_id = server
201 .make_channel(
202 "the-channel",
203 None,
204 (&client_a, cx_a),
205 &mut [(&client_b, cx_b), (&client_c, cx_c)],
206 )
207 .await;
208
209 let channel_chat_a = client_a
210 .channel_store()
211 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
212 .await
213 .unwrap();
214 let channel_chat_b = client_b
215 .channel_store()
216 .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
217 .await
218 .unwrap();
219
220 // Client A sends some messages.
221 channel_chat_a
222 .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
223 .await
224 .unwrap();
225 let msg_id_2 = channel_chat_a
226 .update(cx_a, |c, cx| {
227 c.send_message(
228 MessageParams {
229 text: "two @user_b".to_string(),
230 mentions: vec![(4..12, client_b.id())],
231 reply_to_message_id: None,
232 },
233 cx,
234 )
235 .unwrap()
236 })
237 .await
238 .unwrap();
239 channel_chat_a
240 .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
241 .await
242 .unwrap();
243
244 // Clients A and B see all of the messages.
245 executor.run_until_parked();
246 let expected_messages = &["one", "two @user_b", "three"];
247 assert_messages(&channel_chat_a, expected_messages, cx_a);
248 assert_messages(&channel_chat_b, expected_messages, cx_b);
249
250 // Ensure that client B received a notification for the mention.
251 client_b.notification_store().read_with(cx_b, |store, _| {
252 assert_eq!(store.notification_count(), 2);
253 let entry = store.notification_at(0).unwrap();
254 assert_eq!(
255 entry.notification,
256 Notification::ChannelMessageMention {
257 message_id: msg_id_2,
258 sender_id: client_a.id(),
259 channel_id: channel_id.0,
260 }
261 );
262 });
263
264 // Client A deletes one of their messages.
265 channel_chat_a
266 .update(cx_a, |c, cx| {
267 let ChannelMessageId::Saved(id) = c.message(1).id else {
268 panic!("message not saved")
269 };
270 c.remove_message(id, cx)
271 })
272 .await
273 .unwrap();
274
275 // Client B sees that the message is gone.
276 executor.run_until_parked();
277 let expected_messages = &["one", "three"];
278 assert_messages(&channel_chat_a, expected_messages, cx_a);
279 assert_messages(&channel_chat_b, expected_messages, cx_b);
280
281 // Client C joins the channel chat, and does not see the deleted message.
282 let channel_chat_c = client_c
283 .channel_store()
284 .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
285 .await
286 .unwrap();
287 assert_messages(&channel_chat_c, expected_messages, cx_c);
288
289 // Ensure we remove the notifications when the message is removed
290 client_b.notification_store().read_with(cx_b, |store, _| {
291 // First notification is the channel invitation, second would be the mention
292 // notification, which should now be removed.
293 assert_eq!(store.notification_count(), 1);
294 });
295}
296
297#[track_caller]
298fn assert_messages(chat: &Entity<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
299 assert_eq!(
300 chat.read_with(cx, |chat, _| {
301 chat.messages()
302 .iter()
303 .map(|m| m.body.clone())
304 .collect::<Vec<_>>()
305 }),
306 messages
307 );
308}
309
310#[gpui::test]
311async fn test_channel_message_changes(
312 executor: BackgroundExecutor,
313 cx_a: &mut TestAppContext,
314 cx_b: &mut TestAppContext,
315) {
316 let mut server = TestServer::start(executor.clone()).await;
317 let client_a = server.create_client(cx_a, "user_a").await;
318 let client_b = server.create_client(cx_b, "user_b").await;
319
320 let channel_id = server
321 .make_channel(
322 "the-channel",
323 None,
324 (&client_a, cx_a),
325 &mut [(&client_b, cx_b)],
326 )
327 .await;
328
329 // Client A sends a message, client B should see that there is a new message.
330 let channel_chat_a = client_a
331 .channel_store()
332 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
333 .await
334 .unwrap();
335
336 channel_chat_a
337 .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
338 .await
339 .unwrap();
340
341 executor.run_until_parked();
342
343 let b_has_messages = cx_b.update(|cx| {
344 client_b
345 .channel_store()
346 .read(cx)
347 .has_new_messages(channel_id)
348 });
349
350 assert!(b_has_messages);
351
352 // Opening the chat should clear the changed flag.
353 cx_b.update(|cx| {
354 collab_ui::init(&client_b.app_state, cx);
355 });
356 let project_b = client_b.build_empty_local_project(cx_b);
357 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
358
359 let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new);
360 chat_panel_b
361 .update_in(cx_b, |chat_panel, window, cx| {
362 chat_panel.set_active(true, window, cx);
363 chat_panel.select_channel(channel_id, None, cx)
364 })
365 .await
366 .unwrap();
367
368 executor.run_until_parked();
369
370 let b_has_messages = cx_b.update(|_, cx| {
371 client_b
372 .channel_store()
373 .read(cx)
374 .has_new_messages(channel_id)
375 });
376
377 assert!(!b_has_messages);
378
379 // Sending a message while the chat is open should not change the flag.
380 channel_chat_a
381 .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
382 .await
383 .unwrap();
384
385 executor.run_until_parked();
386
387 let b_has_messages = cx_b.update(|_, cx| {
388 client_b
389 .channel_store()
390 .read(cx)
391 .has_new_messages(channel_id)
392 });
393
394 assert!(!b_has_messages);
395
396 // Sending a message while the chat is closed should change the flag.
397 chat_panel_b.update_in(cx_b, |chat_panel, window, cx| {
398 chat_panel.set_active(false, window, cx);
399 });
400
401 // Sending a message while the chat is open should not change the flag.
402 channel_chat_a
403 .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
404 .await
405 .unwrap();
406
407 executor.run_until_parked();
408
409 let b_has_messages = cx_b.update(|_, cx| {
410 client_b
411 .channel_store()
412 .read(cx)
413 .has_new_messages(channel_id)
414 });
415
416 assert!(b_has_messages);
417
418 // Closing the chat should re-enable change tracking
419 cx_b.update(|_, _| drop(chat_panel_b));
420
421 channel_chat_a
422 .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
423 .await
424 .unwrap();
425
426 executor.run_until_parked();
427
428 let b_has_messages = cx_b.update(|_, cx| {
429 client_b
430 .channel_store()
431 .read(cx)
432 .has_new_messages(channel_id)
433 });
434
435 assert!(b_has_messages);
436}
437
438#[gpui::test]
439async fn test_chat_replies(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
440 let mut server = TestServer::start(cx_a.executor()).await;
441 let client_a = server.create_client(cx_a, "user_a").await;
442 let client_b = server.create_client(cx_b, "user_b").await;
443
444 let channel_id = server
445 .make_channel(
446 "the-channel",
447 None,
448 (&client_a, cx_a),
449 &mut [(&client_b, cx_b)],
450 )
451 .await;
452
453 // Client A sends a message, client B should see that there is a new message.
454 let channel_chat_a = client_a
455 .channel_store()
456 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
457 .await
458 .unwrap();
459
460 let channel_chat_b = client_b
461 .channel_store()
462 .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
463 .await
464 .unwrap();
465
466 let msg_id = channel_chat_a
467 .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
468 .await
469 .unwrap();
470
471 cx_a.run_until_parked();
472
473 let reply_id = channel_chat_b
474 .update(cx_b, |c, cx| {
475 c.send_message(
476 MessageParams {
477 text: "reply".into(),
478 reply_to_message_id: Some(msg_id),
479 mentions: Vec::new(),
480 },
481 cx,
482 )
483 .unwrap()
484 })
485 .await
486 .unwrap();
487
488 cx_a.run_until_parked();
489
490 channel_chat_a.update(cx_a, |channel_chat, _| {
491 assert_eq!(
492 channel_chat
493 .find_loaded_message(reply_id)
494 .unwrap()
495 .reply_to_message_id,
496 Some(msg_id),
497 )
498 });
499}
500
501#[gpui::test]
502async fn test_chat_editing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
503 let mut server = TestServer::start(cx_a.executor()).await;
504 let client_a = server.create_client(cx_a, "user_a").await;
505 let client_b = server.create_client(cx_b, "user_b").await;
506
507 let channel_id = server
508 .make_channel(
509 "the-channel",
510 None,
511 (&client_a, cx_a),
512 &mut [(&client_b, cx_b)],
513 )
514 .await;
515
516 // Client A sends a message, client B should see that there is a new message.
517 let channel_chat_a = client_a
518 .channel_store()
519 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
520 .await
521 .unwrap();
522
523 let channel_chat_b = client_b
524 .channel_store()
525 .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
526 .await
527 .unwrap();
528
529 let msg_id = channel_chat_a
530 .update(cx_a, |c, cx| {
531 c.send_message(
532 MessageParams {
533 text: "Initial message".into(),
534 reply_to_message_id: None,
535 mentions: Vec::new(),
536 },
537 cx,
538 )
539 .unwrap()
540 })
541 .await
542 .unwrap();
543
544 cx_a.run_until_parked();
545
546 channel_chat_a
547 .update(cx_a, |c, cx| {
548 c.update_message(
549 msg_id,
550 MessageParams {
551 text: "Updated body".into(),
552 reply_to_message_id: None,
553 mentions: Vec::new(),
554 },
555 cx,
556 )
557 .unwrap()
558 })
559 .await
560 .unwrap();
561
562 cx_a.run_until_parked();
563 cx_b.run_until_parked();
564
565 channel_chat_a.update(cx_a, |channel_chat, _| {
566 let update_message = channel_chat.find_loaded_message(msg_id).unwrap();
567
568 assert_eq!(update_message.body, "Updated body");
569 assert_eq!(update_message.mentions, Vec::new());
570 });
571 channel_chat_b.update(cx_b, |channel_chat, _| {
572 let update_message = channel_chat.find_loaded_message(msg_id).unwrap();
573
574 assert_eq!(update_message.body, "Updated body");
575 assert_eq!(update_message.mentions, Vec::new());
576 });
577
578 // test mentions are updated correctly
579
580 client_b.notification_store().read_with(cx_b, |store, _| {
581 assert_eq!(store.notification_count(), 1);
582 let entry = store.notification_at(0).unwrap();
583 assert!(matches!(
584 entry.notification,
585 Notification::ChannelInvitation { .. }
586 ),);
587 });
588
589 channel_chat_a
590 .update(cx_a, |c, cx| {
591 c.update_message(
592 msg_id,
593 MessageParams {
594 text: "Updated body including a mention for @user_b".into(),
595 reply_to_message_id: None,
596 mentions: vec![(37..45, client_b.id())],
597 },
598 cx,
599 )
600 .unwrap()
601 })
602 .await
603 .unwrap();
604
605 cx_a.run_until_parked();
606 cx_b.run_until_parked();
607
608 channel_chat_a.update(cx_a, |channel_chat, _| {
609 assert_eq!(
610 channel_chat.find_loaded_message(msg_id).unwrap().body,
611 "Updated body including a mention for @user_b",
612 )
613 });
614 channel_chat_b.update(cx_b, |channel_chat, _| {
615 assert_eq!(
616 channel_chat.find_loaded_message(msg_id).unwrap().body,
617 "Updated body including a mention for @user_b",
618 )
619 });
620 client_b.notification_store().read_with(cx_b, |store, _| {
621 assert_eq!(store.notification_count(), 2);
622 let entry = store.notification_at(0).unwrap();
623 assert_eq!(
624 entry.notification,
625 Notification::ChannelMessageMention {
626 message_id: msg_id,
627 sender_id: client_a.id(),
628 channel_id: channel_id.0,
629 }
630 );
631 });
632
633 // Test update message and keep the mention and check that the body is updated correctly
634
635 channel_chat_a
636 .update(cx_a, |c, cx| {
637 c.update_message(
638 msg_id,
639 MessageParams {
640 text: "Updated body v2 including a mention for @user_b".into(),
641 reply_to_message_id: None,
642 mentions: vec![(37..45, client_b.id())],
643 },
644 cx,
645 )
646 .unwrap()
647 })
648 .await
649 .unwrap();
650
651 cx_a.run_until_parked();
652 cx_b.run_until_parked();
653
654 channel_chat_a.update(cx_a, |channel_chat, _| {
655 assert_eq!(
656 channel_chat.find_loaded_message(msg_id).unwrap().body,
657 "Updated body v2 including a mention for @user_b",
658 )
659 });
660 channel_chat_b.update(cx_b, |channel_chat, _| {
661 assert_eq!(
662 channel_chat.find_loaded_message(msg_id).unwrap().body,
663 "Updated body v2 including a mention for @user_b",
664 )
665 });
666
667 client_b.notification_store().read_with(cx_b, |store, _| {
668 let message = store.channel_message_for_id(msg_id);
669 assert!(message.is_some());
670 assert_eq!(
671 message.unwrap().body,
672 "Updated body v2 including a mention for @user_b"
673 );
674 assert_eq!(store.notification_count(), 2);
675 let entry = store.notification_at(0).unwrap();
676 assert_eq!(
677 entry.notification,
678 Notification::ChannelMessageMention {
679 message_id: msg_id,
680 sender_id: client_a.id(),
681 channel_id: channel_id.0,
682 }
683 );
684 });
685
686 // If we remove a mention from a message the corresponding mention notification
687 // should also be removed.
688
689 channel_chat_a
690 .update(cx_a, |c, cx| {
691 c.update_message(
692 msg_id,
693 MessageParams {
694 text: "Updated body without a mention".into(),
695 reply_to_message_id: None,
696 mentions: vec![],
697 },
698 cx,
699 )
700 .unwrap()
701 })
702 .await
703 .unwrap();
704
705 cx_a.run_until_parked();
706 cx_b.run_until_parked();
707
708 channel_chat_a.update(cx_a, |channel_chat, _| {
709 assert_eq!(
710 channel_chat.find_loaded_message(msg_id).unwrap().body,
711 "Updated body without a mention",
712 )
713 });
714 channel_chat_b.update(cx_b, |channel_chat, _| {
715 assert_eq!(
716 channel_chat.find_loaded_message(msg_id).unwrap().body,
717 "Updated body without a mention",
718 )
719 });
720 client_b.notification_store().read_with(cx_b, |store, _| {
721 // First notification is the channel invitation, second would be the mention
722 // notification, which should now be removed.
723 assert_eq!(store.notification_count(), 1);
724 });
725}