p2p_chat/ui/chat_mode/
render.rs1use 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 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 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 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 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 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 all_items.sort_by_key(|(ordering, _, _, _)| *ordering);
123
124 let total_items = all_items.len();
125 let visible_lines = height as usize;
126
127 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 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 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 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}