1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
use super::{categorize_error, App, ComposerMode, InputMode};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::style::Style;
use tui_textarea::TextArea;
use uuid::Uuid;
use crate::log_reply;
impl App {
// UNIFIED COMPOSER METHODS (using tui-textarea)
// ============================================================================
/// Open composer for new post
pub fn open_composer_new_post(&mut self) {
self.composer_state.mode = Some(ComposerMode::NewPost);
let mut textarea = TextArea::default();
// Enable hard tab indent for better wrapping behavior
textarea.set_hard_tab_indent(true);
// Set styles immediately to avoid rendering glitches
self.apply_composer_styling(&mut textarea);
self.composer_state.textarea = textarea;
self.composer_state.max_chars = 280;
self.input_mode = InputMode::Typing;
}
/// Open composer for reply
pub fn open_composer_reply(
&mut self,
parent_post_id: Uuid,
parent_author: String,
parent_content: String,
) {
log_reply!(
"open_composer_reply: Opening reply composer for post_id={}",
parent_post_id
);
self.composer_state.mode = Some(ComposerMode::Reply {
parent_post_id,
parent_author,
parent_content,
});
let mut textarea = TextArea::default();
// Enable hard tab indent for better wrapping behavior
textarea.set_hard_tab_indent(true);
// Set styles immediately to avoid rendering glitches
self.apply_composer_styling(&mut textarea);
self.composer_state.textarea = textarea;
self.composer_state.max_chars = 280;
self.input_mode = InputMode::Typing;
log_reply!("open_composer_reply: Reply composer opened, input_mode set to Typing");
}
/// Open composer for editing bio
pub fn open_composer_edit_bio(&mut self, current_bio: String) {
self.composer_state.mode = Some(ComposerMode::EditBio);
let mut textarea = TextArea::from(current_bio.lines());
// Enable hard tab indent for better wrapping behavior
textarea.set_hard_tab_indent(true);
// Set styles immediately to avoid rendering glitches
self.apply_composer_styling(&mut textarea);
self.composer_state.textarea = textarea;
self.composer_state.max_chars = 160;
self.input_mode = InputMode::Typing;
}
/// Close composer
pub fn close_composer(&mut self) {
self.composer_state.mode = None;
let mut textarea = TextArea::default();
textarea.set_hard_tab_indent(true);
self.apply_composer_styling(&mut textarea);
self.composer_state.textarea = textarea;
self.input_mode = InputMode::Navigation;
}
/// Apply consistent styling to composer TextArea
fn apply_composer_styling(&self, textarea: &mut TextArea) {
use crate::ui::theme::get_theme_colors;
let theme = get_theme_colors(self);
textarea.set_style(
Style::default().fg(theme.primary), // Use primary color for better visibility
);
textarea.set_cursor_style(
Style::default().fg(theme.background).bg(theme.primary), // Visible cursor
);
textarea.set_cursor_line_style(
Style::default(), // No special cursor line styling
);
}
/// Handle keyboard input for composer (delegates to TextArea)
pub fn handle_composer_input(&mut self, key: KeyEvent) {
log_reply!(
"handle_composer_input: Processing key={:?} for mode={:?}",
key.code,
self.composer_state.mode
);
// Check if this is a character input that would exceed the limit
if let KeyCode::Char(_c) = key.code {
// Check current character count
let current_count = self.composer_state.char_count();
// Only allow input if under the limit
if current_count >= self.composer_state.max_chars {
// Don't process this character - limit reached
return;
}
}
// Convert KeyEvent to tui_textarea::Input and process
use tui_textarea::Input;
let input = Input::from(crossterm::event::Event::Key(key));
self.composer_state.textarea.input(input);
// After input, check if we need to wrap the current line
// This ensures text stays visible within the modal
self.wrap_composer_text_if_needed();
}
/// Wrap text in composer if current line exceeds reasonable width
fn wrap_composer_text_if_needed(&mut self) {
crate::text_wrapper::wrap_textarea_if_needed(
&mut self.composer_state.textarea,
crate::text_wrapper::WrapConfig::COMPOSER,
);
}
/// Submit composer content based on mode
pub async fn submit_composer(&mut self) -> Result<()> {
log_reply!(
"submit_composer: Starting submission, mode={:?}",
self.composer_state.mode
);
let content = self.composer_state.get_content();
let trimmed = content.trim();
// Validate empty input
if trimmed.is_empty() {
match &self.composer_state.mode {
Some(ComposerMode::NewPost) => {
self.posts_state.error =
Some("Validation Error: Cannot post empty content.".to_string());
}
Some(ComposerMode::Reply { .. }) => {
if let Some(detail_state) = &mut self.post_detail_state {
detail_state.error =
Some("Validation Error: Cannot post empty reply.".to_string());
}
}
Some(ComposerMode::EditBio) => {
self.profile_state.error =
Some("Validation Error: Bio cannot be empty.".to_string());
}
None => {}
}
return Ok(());
}
// Validate character limit
let char_count = self.composer_state.char_count();
if char_count > self.composer_state.max_chars {
let error_msg = format!(
"Validation Error: Content exceeds {} characters (current: {})",
self.composer_state.max_chars, char_count
);
match &self.composer_state.mode {
Some(ComposerMode::NewPost) => {
self.posts_state.error = Some(error_msg);
}
Some(ComposerMode::Reply { .. }) => {
if let Some(detail_state) = &mut self.post_detail_state {
detail_state.error = Some(error_msg);
}
}
Some(ComposerMode::EditBio) => {
self.profile_state.error = Some(error_msg);
}
None => {}
}
return Ok(());
}
// Parse emoji shortcodes
let parsed_content = crate::emoji::parse_emoji_shortcodes(&content);
// Submit based on mode
match &self.composer_state.mode {
Some(ComposerMode::NewPost) => {
self.posts_state.error = None;
match self.api_client.create_post(parsed_content).await {
Ok(_) => {
log_reply!("submit_composer: New post successful, closing composer");
self.close_composer();
self.load_posts().await?;
}
Err(e) => {
self.posts_state.error = Some(categorize_error(&e.to_string()));
}
}
}
Some(ComposerMode::Reply { parent_post_id, .. }) => {
let post_id = *parent_post_id;
// Get the root post ID from the modal (the thread we're viewing)
let root_post_id = self
.post_detail_state
.as_ref()
.and_then(|s| s.post.as_ref().map(|p| p.id))
.unwrap_or(post_id);
// Debug logging to file
use std::io::Write;
let mut log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("fido_debug.log")
.ok();
if let Some(ref mut f) = log {
let _ = writeln!(f, "\n=== REPLY SUBMISSION START ===");
let _ = writeln!(
f,
"Before reply - viewing_post_detail={}, show_full_post_modal={}",
self.viewing_post_detail,
self.post_detail_state
.as_ref()
.map(|s| s.show_full_post_modal)
.unwrap_or(false)
);
}
if let Some(detail_state) = &mut self.post_detail_state {
detail_state.error = None;
}
match self.api_client.create_reply(post_id, parsed_content).await {
Ok(new_reply) => {
let new_reply_id = new_reply.id;
if let Some(ref mut f) = log {
let _ = writeln!(
f,
"Reply created successfully, new_reply_id={}",
new_reply_id
);
}
// Optimistic update: increment reply count in cached post
if let Some(cached_post) =
self.posts_state.posts.iter_mut().find(|p| p.id == post_id)
{
cached_post.reply_count += 1;
}
log_reply!("submit_composer: Reply successful, will close composer after modal restoration");
// Ensure we stay in thread view
self.viewing_post_detail = true;
if let Some(ref mut f) = log {
let _ = writeln!(
f,
"Before load_post_detail - root_post_id={}",
root_post_id
);
}
// Reload the root thread, not the parent post
self.load_post_detail(root_post_id).await?;
if let Some(ref mut f) = log {
let _ = writeln!(f, "After load_post_detail - viewing_post_detail={}, show_full_post_modal={}, post_detail_state.is_some()={}",
self.viewing_post_detail,
self.post_detail_state.as_ref().map(|s| s.show_full_post_modal).unwrap_or(false),
self.post_detail_state.is_some());
}
// Explicitly ensure modal is open after reload
if let Some(detail_state) = &mut self.post_detail_state {
detail_state.show_full_post_modal = true;
detail_state.full_post_modal_id = Some(root_post_id);
if let Some(ref mut f) = log {
let _ = writeln!(f, "Explicitly set show_full_post_modal=true");
}
}
// Select the newly created reply in the modal
self.select_reply_in_modal(new_reply_id);
// Close composer AFTER all modal state has been restored
log_reply!("submit_composer: Modal state restored, now closing composer");
self.close_composer();
if let Some(ref mut f) = log {
let _ = writeln!(
f,
"After close_composer - viewing_post_detail={}",
self.viewing_post_detail
);
let _ = writeln!(
f,
"Final state - viewing_post_detail={}, show_full_post_modal={}",
self.viewing_post_detail,
self.post_detail_state
.as_ref()
.map(|s| s.show_full_post_modal)
.unwrap_or(false)
);
let _ = writeln!(f, "=== REPLY SUBMISSION END ===\n");
}
}
Err(e) => {
log_reply!("submit_composer: Reply failed with error: {}", e);
if let Some(detail_state) = &mut self.post_detail_state {
detail_state.error = Some(categorize_error(&e.to_string()));
}
}
}
}
Some(ComposerMode::EditBio) => {
if let Some(user) = &self.auth_state.current_user {
self.profile_state.error = None;
match self.api_client.update_bio(user.id, parsed_content).await {
Ok(_) => {
self.close_composer();
self.load_profile().await?;
}
Err(e) => {
let error_msg = e.to_string();
let parsed_error = if error_msg.contains("401")
|| error_msg.contains("403")
{
"Authorization Error: You can only edit your own profile"
.to_string()
} else if error_msg.contains("400") {
format!("Validation Error: {}", error_msg)
} else if error_msg.contains("connection")
|| error_msg.contains("timeout")
{
"Network Error: Connection failed - check your network and try again"
.to_string()
} else {
format!("Failed to update bio: {}", error_msg)
};
self.profile_state.error = Some(parsed_error);
}
}
}
}
None => {}
}
Ok(())
}
}