p2p_chat/ui/chat_mode/
render.rs

1//! This module contains the rendering logic for the chat UI mode.
2use super::super::UIState;
3use super::ChatMode;
4use anyhow::Result;
5use chrono::{DateTime, Local, Utc};
6use crossterm::{
7    cursor, queue,
8    style::{Color, Print, ResetColor, SetForegroundColor},
9};
10use std::io::Write;
11
12impl ChatMode {
13    /// Renders the chat interface, including messages and suggestions.
14    ///
15    /// This function draws the chat history, input buffer, and any active
16    /// suggestions to the terminal. It handles scrolling and message formatting.
17    ///
18    /// # Arguments
19    ///
20    /// * `stdout` - A mutable reference to the output stream.
21    /// * `state` - The current UI state.
22    /// * `area` - The (x, y, width, height) coordinates of the rendering area.
23    /// * `node` - An optional reference to the application's `Node` for message decryption and friend information.
24    ///
25    /// # Errors
26    ///
27    /// This function returns an error if writing to the output stream fails.
28    pub fn render(
29        &self,
30        stdout: &mut impl Write,
31        state: &UIState,
32        area: (u16, u16, u16, u16),
33        node: Option<&crate::cli::commands::Node>,
34    ) -> Result<()> {
35        let (x, y, width, height) = area;
36
37        let mut all_items: Vec<(DateTime<Utc>, DateTime<Local>, String, Color)> = Vec::new();
38
39        // Process stored messages
40        for entry in &state.messages {
41            let message = &entry.message;
42            let message_timestamp =
43                DateTime::<Utc>::from_timestamp_millis(message.timestamp).unwrap_or_else(Utc::now);
44            let display_timestamp = message_timestamp.with_timezone(&Local);
45
46            let content = if let Some(node) = node {
47                let is_sent = message.sender == node.identity.peer_id;
48                let other_peer = if is_sent {
49                    message.recipient
50                } else {
51                    message.sender
52                };
53
54                // Decrypt message content if a node is provided
55                match tokio::task::block_in_place(|| {
56                    tokio::runtime::Handle::current().block_on(node.friends.get_friend(&other_peer))
57                }) {
58                    Ok(Some(friend)) => {
59                        // Attempt to decrypt the message content.
60                        match node
61                            .identity
62                            .decrypt_from(&friend.e2e_public_key, &message.content)
63                        {
64                            Ok(plaintext) => String::from_utf8_lossy(&plaintext).to_string(),
65                            Err(_) => "[Decryption Failed]".to_string(),
66                        }
67                    }
68                    Ok(None) => "[Unknown Peer]".to_string(),
69                    Err(_) => "[Database Error]".to_string(),
70                }
71            } else {
72                "[Encrypted]".to_string()
73            };
74
75            let (text, color) = if node
76                .map(|n| message.sender == n.identity.peer_id)
77                .unwrap_or(false)
78            {
79                (format!("You: {}", content), Color::Green)
80            } else {
81                let sender_display = if let Some(node) = node {
82                    match tokio::task::block_in_place(|| {
83                        tokio::runtime::Handle::current()
84                            .block_on(node.friends.get_friend(&message.sender))
85                    }) {
86                        Ok(Some(friend)) => friend.nickname.unwrap_or_else(|| {
87                            let peer_str = message.sender.to_string();
88                            if peer_str.chars().count() > 8 {
89                                format!("{}...", peer_str.chars().take(8).collect::<String>())
90                            } else {
91                                peer_str
92                            }
93                        }),
94                        _ => {
95                            let peer_str = message.sender.to_string();
96                            if peer_str.chars().count() > 8 {
97                                format!("{}...", peer_str.chars().take(8).collect::<String>())
98                            } else {
99                                peer_str
100                            }
101                        }
102                    }
103                } else {
104                    message.sender.to_string()
105                };
106
107                (format!("{}: {}", sender_display, content), Color::Cyan)
108            };
109
110            all_items.push((entry.received_at, display_timestamp, text, color));
111        }
112
113        // Add chat messages from UI state (e.g., system messages)
114        for (timestamp, chat_msg) in &state.chat_messages {
115            let local_timestamp = timestamp.with_timezone(&Local);
116            for line in chat_msg.lines() {
117                all_items.push((*timestamp, local_timestamp, line.to_string(), Color::White));
118            }
119        }
120
121        // Sort all items chronologically for display
122        all_items.sort_by_key(|(ordering, _, _, _)| *ordering);
123
124        let total_items = all_items.len();
125        let visible_lines = height as usize;
126
127        // Calculate visible range based on scroll offset
128        let start_idx = if total_items > visible_lines {
129            if state.scroll_offset >= total_items {
130                0
131            } else {
132                total_items.saturating_sub(visible_lines + state.scroll_offset)
133            }
134        } else {
135            0
136        };
137
138        let end_idx = (start_idx + visible_lines).min(total_items);
139
140        // Render visible items
141        for (line_idx, item_idx) in (start_idx..end_idx).enumerate() {
142            if let Some((_, timestamp, text, color)) = all_items.get(item_idx) {
143                queue!(stdout, cursor::MoveTo(x, y + line_idx as u16))?;
144
145                let full_text = if text.starts_with("__HISTORY_OUTPUT__") {
146                    // Special handling for history output without timestamp
147                    text.trim_start_matches("__HISTORY_OUTPUT__").to_string()
148                } else {
149                    let time_str = timestamp.format("%H:%M:%S").to_string();
150                    format!("[{}] {}", time_str, text)
151                };
152
153                let display_text = if state.horizontal_scroll_offset < full_text.chars().count() {
154                    full_text
155                        .chars()
156                        .skip(state.horizontal_scroll_offset)
157                        .collect::<String>()
158                } else {
159                    String::new()
160                };
161
162                queue!(
163                    stdout,
164                    SetForegroundColor(*color),
165                    Print(display_text),
166                    ResetColor
167                )?;
168            }
169        }
170
171        // Render scroll indicators
172        if state.scroll_offset > 0 {
173            queue!(
174                stdout,
175                cursor::MoveTo(x + width - 10, y),
176                SetForegroundColor(Color::Yellow),
177                Print(format!("↑ +{} more", state.scroll_offset)),
178                ResetColor
179            )?;
180        }
181
182        if state.horizontal_scroll_offset > 0 {
183            queue!(
184                stdout,
185                cursor::MoveTo(x, y),
186                SetForegroundColor(Color::Yellow),
187                Print(format!("← +{}", state.horizontal_scroll_offset)),
188                ResetColor
189            )?;
190        }
191
192        Ok(())
193    }
194}