1use std::sync::Arc;
2
3use crossterm::event::{Event, KeyCode, KeyModifiers};
4use futures_util::StreamExt;
5use imbl::Vector;
6use input::MessageOrCommand;
7use invited_room::InvitedRoomView;
8use matrix_sdk::{
9 Client, Room, RoomState,
10 locks::Mutex,
11 ruma::{
12 OwnedEventId, OwnedRoomId, RoomId, UserId,
13 api::client::receipt::create_receipt::v3::ReceiptType,
14 events::room::message::RoomMessageEventContent,
15 },
16};
17use matrix_sdk_ui::{
18 Timeline,
19 timeline::{TimelineBuilder, TimelineFocus, TimelineItem},
20};
21use ratatui::{prelude::*, widgets::*};
22use tokio::{spawn, sync::OnceCell, task::JoinHandle};
23use tracing::info;
24
25use self::{details::RoomDetails, input::Input, timeline::TimelineView};
26use super::status::StatusHandle;
27use crate::{
28 HEADER_BG, NORMAL_ROW_COLOR, TEXT_COLOR, Timelines,
29 widgets::{recovery::ShouldExit, room_view::timeline::TimelineListState},
30};
31
32mod details;
33mod input;
34mod invited_room;
35mod timeline;
36
37const DEFAULT_TILING_DIRECTION: Direction = Direction::Horizontal;
38
39pub struct DetailsState<'a> {
40 selected_room: Option<&'a Room>,
41 selected_item: Option<Arc<TimelineItem>>,
42}
43
44enum Mode {
45 Normal { invited_room_view: Option<InvitedRoomView> },
46 Details { tiling_direction: Direction, view: RoomDetails },
47}
48
49enum TimelineKind {
50 Room {
51 room: Option<OwnedRoomId>,
52 },
53
54 Thread {
55 room: OwnedRoomId,
56 thread_root: OwnedEventId,
57 timeline: Arc<OnceCell<Arc<Timeline>>>,
59 items: Arc<Mutex<Vector<Arc<TimelineItem>>>>,
62 task: JoinHandle<()>,
65 },
66}
67
68pub struct RoomView {
69 client: Client,
70
71 timelines: Timelines,
73
74 status_handle: StatusHandle,
75
76 current_pagination: Arc<Mutex<Option<JoinHandle<()>>>>,
77
78 mode: Mode,
79 kind: TimelineKind,
80
81 timeline_list: TimelineListState,
82
83 input: Input,
84}
85
86impl RoomView {
87 pub fn new(client: Client, timelines: Timelines, status_handle: StatusHandle) -> Self {
88 Self {
89 client,
90 timelines,
91 status_handle,
92 current_pagination: Default::default(),
93 mode: Mode::Normal { invited_room_view: None },
94 kind: TimelineKind::Room { room: None },
95 input: Input::new(),
96 timeline_list: TimelineListState::default(),
97 }
98 }
99
100 fn switch_to_room_timeline(&mut self, room: Option<OwnedRoomId>) {
101 match &mut self.kind {
102 TimelineKind::Room { room: prev_room } => {
103 self.kind = TimelineKind::Room { room: room.or(prev_room.take()) };
104 }
105 TimelineKind::Thread { task, room, .. } => {
106 task.abort();
108 self.kind = TimelineKind::Room { room: Some(room.clone()) };
109 }
110 }
111 }
112
113 fn switch_to_thread_timeline(&mut self) {
114 let Some(room) = self.room() else {
115 return;
116 };
117
118 let Some(timeline_list_nth) = self.timeline_list.selected() else {
119 return;
120 };
121
122 let Some(items) = self.get_selected_timeline_items() else {
123 self.status_handle.set_message("missing timeline for room".to_owned());
124 return;
125 };
126
127 let Some(root_event) = items.get(timeline_list_nth).and_then(|item| item.as_event()) else {
128 self.status_handle.set_message("no event associated to this timeline item".to_owned());
129 return;
130 };
131
132 if root_event.content().as_message().is_none() {
133 self.status_handle.set_message("this event can't be a thread start!".to_owned());
134 return;
135 }
136
137 let Some(root_event_id) = root_event.event_id().map(ToOwned::to_owned) else {
138 self.status_handle.set_message("can't open thread on a local echo".to_owned());
139 return;
140 };
141
142 info!("Opening thread view for event {root_event_id} in room {}", room.room_id());
143
144 let thread_timeline = Arc::new(OnceCell::new());
145 let items = Arc::new(Mutex::new(Default::default()));
146
147 let i = items.clone();
148 let t = thread_timeline.clone();
149 let root = root_event_id;
150 let cloned_root = root.clone();
151 let r = room.clone();
152 let task = spawn(async move {
153 let timeline = TimelineBuilder::new(&r)
154 .with_focus(TimelineFocus::Thread { root_event_id: cloned_root })
155 .track_read_marker_and_receipts()
156 .build()
157 .await
158 .unwrap();
159
160 let items = i;
161 let (initial_items, mut stream) = timeline.subscribe().await;
162
163 t.set(Arc::new(timeline)).unwrap();
164 *items.lock() = initial_items;
165
166 while let Some(diffs) = stream.next().await {
167 let mut items = items.lock();
168 for diff in diffs {
169 diff.apply(&mut items);
170 }
171 }
172 });
173
174 self.timeline_list.unselect();
175
176 self.kind = TimelineKind::Thread {
177 thread_root: root,
178 room: room.room_id().to_owned(),
179 timeline: thread_timeline,
180 items,
181 task,
182 };
183 }
184
185 fn room_id(&self) -> Option<&RoomId> {
186 match &self.kind {
187 TimelineKind::Room { room } => room.as_deref(),
188 TimelineKind::Thread { room, .. } => Some(room),
189 }
190 }
191
192 pub fn room(&self) -> Option<Room> {
194 self.room_id().and_then(|room_id| self.client.get_room(room_id))
195 }
196
197 pub async fn handle_event(&mut self, event: Event) {
198 use KeyCode::*;
199
200 match &mut self.mode {
201 Mode::Normal { invited_room_view } => {
202 if let Some(view) = invited_room_view {
203 view.handle_event(event);
204 } else if let Event::Key(key) = event {
205 match (key.modifiers, key.code) {
206 (KeyModifiers::NONE, Enter) => {
207 if !self.input.is_empty() {
208 let message_or_command = self.input.get_input();
209
210 match message_or_command {
211 Ok(MessageOrCommand::Message(message)) => {
212 self.send_message(message).await
213 }
214 Ok(MessageOrCommand::Command(command)) => {
215 self.handle_command(command).await
216 }
217 Err(e) => {
218 self.status_handle.set_message(e.render().to_string());
219 self.input.clear();
220 }
221 }
222 }
223 }
224
225 (KeyModifiers::NONE, Esc)
228 if matches!(self.kind, TimelineKind::Thread { .. }) =>
229 {
230 self.switch_to_room_timeline(None);
231 }
232
233 (KeyModifiers::ALT, Char('s')) => {
236 self.print_thread_subscription_status().await;
237 }
238
239 (KeyModifiers::CONTROL, Char('l')) => {
240 self.toggle_reaction_to_latest_msg().await
241 }
242
243 (KeyModifiers::NONE, PageUp) => self.back_paginate(),
244
245 (KeyModifiers::ALT, Char('e')) => {
246 if let TimelineKind::Room { room: Some(_) } = self.kind {
247 self.mode = Mode::Details {
248 tiling_direction: DEFAULT_TILING_DIRECTION,
249 view: RoomDetails::with_events_as_selected(),
250 }
251 }
252 }
253
254 (KeyModifiers::ALT, Char('r')) => {
255 if let TimelineKind::Room { room: Some(_) } = self.kind {
256 self.mode = Mode::Details {
257 tiling_direction: DEFAULT_TILING_DIRECTION,
258 view: RoomDetails::with_receipts_as_selected(),
259 }
260 }
261 }
262
263 (KeyModifiers::ALT, Char('l')) => {
264 if let TimelineKind::Room { room: Some(_) } = self.kind {
265 self.mode = Mode::Details {
266 tiling_direction: DEFAULT_TILING_DIRECTION,
267 view: RoomDetails::with_chunks_as_selected(),
268 }
269 }
270 }
271
272 (_, Down) | (KeyModifiers::CONTROL, Char('n')) => {
273 self.timeline_list.select_next()
274 }
275 (_, Up) | (KeyModifiers::CONTROL, Char('p')) => {
276 self.timeline_list.select_previous()
277 }
278 (_, Esc) => self.timeline_list.unselect(),
279
280 (KeyModifiers::CONTROL, Char('t'))
281 if matches!(self.kind, TimelineKind::Room { .. }) =>
282 {
283 self.switch_to_thread_timeline();
284 }
285
286 _ => self.input.handle_key_press(key),
287 }
288 }
289 }
290
291 Mode::Details { view, tiling_direction } => {
292 if let Event::Key(key) = event {
293 match (key.modifiers, key.code) {
294 (KeyModifiers::NONE, PageUp) => self.back_paginate(),
295
296 (KeyModifiers::ALT, Char('t')) => {
297 let new_layout = match tiling_direction {
298 Direction::Horizontal => Direction::Vertical,
299 Direction::Vertical => Direction::Horizontal,
300 };
301
302 *tiling_direction = new_layout;
303 }
304
305 (KeyModifiers::ALT, Char('e')) => {
306 self.mode = Mode::Details {
307 tiling_direction: *tiling_direction,
308 view: RoomDetails::with_events_as_selected(),
309 }
310 }
311
312 (KeyModifiers::ALT, Char('r')) => {
313 self.mode = Mode::Details {
314 tiling_direction: *tiling_direction,
315 view: RoomDetails::with_receipts_as_selected(),
316 }
317 }
318
319 (KeyModifiers::ALT, Char('l')) => {
320 self.mode = Mode::Details {
321 tiling_direction: *tiling_direction,
322 view: RoomDetails::with_chunks_as_selected(),
323 }
324 }
325
326 (_, Down) | (KeyModifiers::CONTROL, Char('n')) => {
327 self.timeline_list.select_next()
328 }
329
330 (_, Up) | (KeyModifiers::CONTROL, Char('p')) => {
331 self.timeline_list.select_previous()
332 }
333
334 _ => match view.handle_key_press(key) {
335 ShouldExit::No => {}
336 ShouldExit::OnlySubScreen => {}
337 ShouldExit::Yes => self.mode = Mode::Normal { invited_room_view: None },
338 },
339 }
340 }
341 }
342 }
343 }
344
345 pub fn set_selected_room(&mut self, room_id: Option<OwnedRoomId>) {
346 if let Some(room_id) = room_id.as_deref() {
347 let maybe_room = self.client.get_room(room_id);
348
349 if let Some(room) = maybe_room {
350 self.switch_to_room_timeline(Some(room_id.to_owned()));
351
352 if matches!(room.state(), RoomState::Invited) {
353 let view = InvitedRoomView::new(room);
354 self.mode = Mode::Normal { invited_room_view: Some(view) };
355 } else {
356 match &mut self.mode {
357 Mode::Normal { invited_room_view } => {
358 invited_room_view.take();
359 }
360 Mode::Details { .. } => {}
361 }
362 }
363 }
364 }
365
366 self.timeline_list = TimelineListState::default();
367 }
368
369 fn get_selected_timeline(&self) -> Option<Arc<Timeline>> {
370 match &self.kind {
371 TimelineKind::Room { room } => room
372 .as_deref()
373 .and_then(|room_id| Some(self.timelines.lock().get(room_id)?.timeline.clone())),
374 TimelineKind::Thread { timeline, .. } => timeline.get().cloned(),
375 }
376 }
377
378 fn get_selected_timeline_items(&self) -> Option<Vector<Arc<TimelineItem>>> {
379 match &self.kind {
380 TimelineKind::Room { room } => room
381 .as_deref()
382 .and_then(|room_id| Some(self.timelines.lock().get(room_id)?.items.lock().clone())),
383 TimelineKind::Thread { items, .. } => Some(items.lock().clone()),
384 }
385 }
386
387 pub fn back_paginate(&mut self) {
390 let Some(sdk_timeline) = self.get_selected_timeline() else {
391 self.status_handle.set_message("missing timeline for room".to_owned());
392 return;
393 };
394
395 let mut pagination = self.current_pagination.lock();
396
397 if let Some(prev) = pagination.take() {
399 prev.abort();
400 }
401
402 let status_handle = self.status_handle.clone();
403
404 *pagination = Some(spawn(async move {
406 if let Err(err) = sdk_timeline.paginate_backwards(5).await {
407 status_handle.set_message(format!("Error during backpagination: {err}"));
408 }
409 }));
410 }
411
412 pub async fn toggle_reaction_to_latest_msg(&mut self) {
413 let Some((sdk_timeline, items)) =
414 self.get_selected_timeline().zip(self.get_selected_timeline_items())
415 else {
416 self.status_handle.set_message("missing timeline for room".to_owned());
417 return;
418 };
419
420 let Some(item_id) = items.iter().rev().find_map(|it| {
422 let event_item = it.as_event()?;
423 event_item.content().as_message()?;
424 Some(event_item.identifier())
425 }) else {
426 self.status_handle.set_message("no item to react to".to_owned());
427 return;
428 };
429
430 match sdk_timeline.toggle_reaction(&item_id, "🥰").await {
432 Ok(_) => {
433 self.status_handle.set_message("reaction sent!".to_owned());
434 }
435 Err(err) => self.status_handle.set_message(format!("error when reacting: {err}")),
436 }
437 }
438
439 async fn call_with_room(&self, function: impl AsyncFnOnce(Room, &StatusHandle)) {
442 if let Some(room) = self.room() {
443 function(room, &self.status_handle).await
444 } else {
445 self.status_handle
446 .set_message("Couldn't find a room selected room to perform an action".to_owned());
447 }
448 }
449
450 async fn invite_member(&mut self, user_id: &str) {
451 self.call_with_room(async move |room, status_handle| {
452 let user_id = match UserId::parse_with_server_name(
453 user_id,
454 room.client().user_id().unwrap().server_name(),
455 ) {
456 Ok(user_id) => user_id,
457 Err(e) => {
458 status_handle
459 .set_message(format!("Failed to parse {user_id} as a user ID: {e:?}"));
460 return;
461 }
462 };
463
464 match room.invite_user_by_id(&user_id).await {
465 Ok(_) => {
466 status_handle
467 .set_message(format!("Successfully invited {user_id} to the room"));
468 }
469 Err(e) => {
470 status_handle
471 .set_message(format!("Failed to invite {user_id} to the room: {e:?}"));
472 }
473 }
474 })
475 .await;
476
477 self.input.clear();
478 }
479
480 async fn leave_room(&mut self) {
481 self.call_with_room(async |room, status_handle| {
482 let _ = room.leave().await.inspect_err(|e| {
483 status_handle.set_message(format!("Couldn't leave the room {e:?}"))
484 });
485 })
486 .await;
487
488 self.input.clear();
489 }
490
491 async fn subscribe_thread(&mut self) {
492 if let TimelineKind::Thread { thread_root, .. } = &self.kind {
493 self.call_with_room(async |room, status_handle| {
494 if let Err(err) = room.subscribe_thread(thread_root.clone(), None).await {
495 status_handle.set_message(format!("error when subscribing to a thread: {err}"));
496 } else {
497 status_handle.set_message("Subscribed to thread!".to_owned());
498 }
499 })
500 .await;
501
502 self.input.clear();
503 }
504 }
505
506 async fn unsubscribe_thread(&mut self) {
507 if let TimelineKind::Thread { thread_root, .. } = &self.kind {
508 self.call_with_room(async |room, status_handle| {
509 if let Err(err) = room.unsubscribe_thread(thread_root.clone()).await {
510 status_handle
511 .set_message(format!("error when unsubscribing to a thread: {err}"));
512 } else {
513 status_handle.set_message("Unsubscribed from thread!".to_owned());
514 }
515 })
516 .await;
517
518 self.input.clear();
519 }
520 }
521
522 async fn print_thread_subscription_status(&mut self) {
523 if let TimelineKind::Thread { thread_root, .. } = &self.kind {
524 self.call_with_room(async |room, status_handle| {
525 match room.fetch_thread_subscription(thread_root.clone()).await {
526 Ok(Some(subscription)) => {
527 status_handle.set_message(format!(
528 "Thread subscription status: {}",
529 if subscription.automatic {
530 "subscribed (automatic)"
531 } else {
532 "subscribed (manual)"
533 }
534 ));
535 }
536 Ok(None) => {
537 status_handle
538 .set_message("Thread is not subscribed or does not exist".to_owned());
539 }
540 Err(err) => {
541 status_handle
542 .set_message(format!("Error getting thread subscription: {err}"));
543 }
544 }
545 })
546 .await;
547 }
548 }
549
550 async fn handle_command(&mut self, command: input::Command) {
551 match command {
552 input::Command::Invite { user_id } => self.invite_member(&user_id).await,
553 input::Command::Leave => self.leave_room().await,
554 input::Command::Subscribe => self.subscribe_thread().await,
555 input::Command::Unsubscribe => self.unsubscribe_thread().await,
556 }
557 }
558
559 async fn send_message(&mut self, message: String) {
560 if let Some(sdk_timeline) = self.get_selected_timeline() {
561 match sdk_timeline.send(RoomMessageEventContent::text_plain(message).into()).await {
562 Ok(_) => {
563 self.input.clear();
564 }
565 Err(err) => {
566 self.status_handle.set_message(format!("error when sending event: {err}"));
567 }
568 }
569 } else {
570 self.status_handle.set_message("missing timeline for room".to_owned());
571 }
572 }
573
574 pub async fn mark_as_read(&mut self) {
576 let Some(sdk_timeline) = self.get_selected_timeline() else {
577 self.status_handle.set_message("missing timeline for room".to_owned());
578 return;
579 };
580
581 match sdk_timeline.mark_as_read(ReceiptType::Read).await {
582 Ok(did) => {
583 self.status_handle.set_message(format!(
584 "did {}send a read receipt!",
585 if did { "" } else { "not " }
586 ));
587 }
588 Err(err) => {
589 self.status_handle.set_message(format!("error when marking a room as read: {err}"));
590 }
591 }
592 }
593
594 fn get_selected_event(&self) -> Option<Arc<TimelineItem>> {
595 let selected = self.timeline_list.selected()?;
596 let items = self.get_selected_timeline_items()?;
597 items.get(selected).cloned()
598 }
599
600 fn update(&mut self) {
601 match &mut self.mode {
602 Mode::Normal { invited_room_view } => {
603 if let Some(view) = invited_room_view
604 && view.should_switch()
605 {
606 self.mode = Mode::Normal { invited_room_view: None };
607 }
608 }
609 Mode::Details { .. } => {}
610 }
611 }
612}
613
614impl Widget for &mut RoomView {
615 fn render(self, area: Rect, buf: &mut Buffer)
616 where
617 Self: Sized,
618 {
619 self.update();
620
621 let vertical =
623 Layout::vertical([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]);
624 let [header_area, middle_area, input_area] = vertical.areas(area);
625
626 let is_thread_view = matches!(self.kind, TimelineKind::Thread { .. });
627 let title = if is_thread_view { "Thread view" } else { "Room view" };
628
629 let header_block = Block::default()
630 .borders(Borders::NONE)
631 .fg(TEXT_COLOR)
632 .bg(HEADER_BG)
633 .title(title)
634 .title_alignment(Alignment::Center);
635
636 let middle_block = Block::default()
637 .border_set(symbols::border::THICK)
638 .bg(NORMAL_ROW_COLOR)
639 .padding(Padding::horizontal(1));
640
641 header_block.render(header_area, buf);
643 middle_block.render(middle_area, buf);
644
645 let render_paragraph = |buf: &mut Buffer, content: String| {
647 Paragraph::new(content)
648 .fg(TEXT_COLOR)
649 .wrap(Wrap { trim: false })
650 .render(middle_area, buf);
651 };
652
653 if let Some(room_id) = self.room_id() {
654 let maybe_room = self.client.get_room(room_id);
655 let mut maybe_room = maybe_room.as_ref();
656
657 let selected_event = self.get_selected_event();
658
659 let timeline_area = match &mut self.mode {
660 Mode::Normal { invited_room_view } => {
661 if let Some(view) = invited_room_view {
662 view.render(middle_area, buf);
663
664 None
665 } else {
666 self.input.render(input_area, buf, &mut maybe_room);
667 Some(middle_area)
668 }
669 }
670
671 Mode::Details { tiling_direction, view } => {
672 let vertical = Layout::new(
673 *tiling_direction,
674 [Constraint::Percentage(50), Constraint::Percentage(50)],
675 );
676 let [timeline_area, details_area] = vertical.areas(middle_area);
677 Clear.render(details_area, buf);
678
679 let mut state =
680 DetailsState { selected_room: maybe_room, selected_item: selected_event };
681
682 view.render(details_area, buf, &mut state);
683
684 Some(timeline_area)
685 }
686 };
687
688 if let Some(timeline_area) = timeline_area
689 && let Some(items) = self.get_selected_timeline_items()
690 {
691 let is_thread = matches!(self.kind, TimelineKind::Thread { .. });
692 let mut timeline = TimelineView::new(&items, is_thread);
693 timeline.render(timeline_area, buf, &mut self.timeline_list);
694 }
695 } else {
696 render_paragraph(buf, "Nothing to see here...".to_owned())
697 }
698 }
699}