use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
use super::super::components::empty_state::render_empty_state;
use super::super::components::footer::render_footer;
use super::super::components::search_bar::{render_search_bar, SearchBarConfig, SearchBarMode};
use super::super::theme::get_theme_colors;
use super::social_components::*;
use crate::app::App;
impl UserListItem for crate::api::SocialUserInfo {
fn username(&self) -> &str {
&self.username
}
fn follower_count(&self) -> Option<usize> {
Some(self.follower_count)
}
fn following_count(&self) -> Option<usize> {
Some(self.following_count)
}
}
impl UserListItem for crate::app::UserSearchResult {
fn username(&self) -> &str {
&self.username
}
}
impl UserListItem for crate::app::UserInfo {
fn username(&self) -> &str {
&self.username
}
fn follower_count(&self) -> Option<usize> {
Some(self.follower_count)
}
fn following_count(&self) -> Option<usize> {
Some(self.following_count)
}
}
impl<'a> UserListItem for &'a crate::app::UserInfo {
fn username(&self) -> &str {
&self.username
}
fn follower_count(&self) -> Option<usize> {
Some(self.follower_count)
}
fn following_count(&self) -> Option<usize> {
Some(self.following_count)
}
}
pub fn render_friends_modal(frame: &mut Frame, app: &mut App, area: Rect) {
let theme = get_theme_colors(app);
let config = ModalConfig::new(" Social Connections ").with_size(70, 80);
let inner = render_modal_container(frame, area, &config, &theme);
if app.friends_state.loading {
render_loading_state(frame, inner, "Loading...", &theme);
return;
}
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(inner);
let selected_tab_index = match app.friends_state.selected_tab {
crate::app::SocialTab::Following => 0,
crate::app::SocialTab::Followers => 1,
crate::app::SocialTab::MutualFriends => 2,
};
let tab_config = TabBarConfig {
tabs: &["Following", "Followers", "Mutual Friends"],
selected_index: selected_tab_index,
};
render_tab_bar(frame, chunks[0], &tab_config, &theme);
let search_config = SearchBarConfig {
query: &app.friends_state.search_query,
is_active: app.friends_state.search_mode,
placeholder: "Press / to search",
mode: SearchBarMode::Slash,
};
render_search_bar(frame, chunks[1], &search_config, &theme);
let filtered_users = app.get_filtered_social_list();
if filtered_users.is_empty() {
let empty_msg = if app.friends_state.search_query.is_empty() {
match app.friends_state.selected_tab {
crate::app::SocialTab::Following => "Not following anyone yet",
crate::app::SocialTab::Followers => "No followers yet",
crate::app::SocialTab::MutualFriends => "No mutual friends yet",
}
} else {
"No users match your search"
};
render_empty_state(frame, chunks[2], empty_msg, &theme);
} else {
let list_config = UserListConfig {
selected_index: app.friends_state.selected_index,
show_stats: true,
};
render_user_list(frame, chunks[2], &filtered_users, &list_config, &theme);
}
let footer_text = if app.friends_state.search_mode {
"Type to search | Esc: Exit search"
} else {
"↑/↓/j/k: Navigate | p: View Profile | f: Follow/Unfollow | /: Search | Tab: Switch | Esc: Close"
};
render_footer(frame, chunks[3], footer_text, &theme);
}
pub fn render_user_profile_view(frame: &mut Frame, app: &App, area: Rect) {
let theme = get_theme_colors(app);
let profile = match &app.user_profile_view {
Some(p) => p,
None => return,
};
let config = ModalConfig::new(" User Profile ").with_size(60, 70);
let inner = render_modal_container(frame, area, &config, &theme);
let modal_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5), Constraint::Length(4), Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(inner);
let header_lines = vec![
Line::from(Span::styled(
format!("@{}", profile.username),
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::styled(
format!("{} ", profile.follower_count),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled("Followers ", Style::default().fg(theme.text_dim)),
Span::styled(
format!("{} ", profile.following_count),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled("Following ", Style::default().fg(theme.text_dim)),
Span::styled(
format!("{} ", profile.post_count),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled("Posts", Style::default().fg(theme.text_dim)),
]),
];
let header = Paragraph::new(header_lines)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border)),
);
frame.render_widget(header, modal_chunks[0]);
let bio_text = profile.bio.as_deref().unwrap_or("No bio");
let bio = Paragraph::new(bio_text)
.wrap(Wrap { trim: true })
.style(Style::default().fg(theme.text))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.title(" Bio "),
);
frame.render_widget(bio, modal_chunks[1]);
let (status_text, status_color) = ("Not Following", theme.text_dim);
let status = Paragraph::new(Line::from(Span::styled(
status_text,
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
)))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border)),
);
frame.render_widget(status, modal_chunks[2]);
render_footer(frame, modal_chunks[4], "Esc: Cancel", &theme);
}
pub fn render_new_conversation_modal(frame: &mut Frame, app: &mut App, area: Rect) {
let theme = get_theme_colors(app);
let config = ModalConfig::new(" New Conversation ").with_size(70, 80);
let inner = render_modal_container(frame, area, &config, &theme);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(inner);
let search_config = SearchBarConfig {
query: &app.dms_state.new_conversation_search_query,
is_active: app.dms_state.new_conversation_search_mode,
placeholder: "Press / to search",
mode: SearchBarMode::Slash,
};
render_search_bar(frame, chunks[0], &search_config, &theme);
let filtered_users = app.get_filtered_mutual_friends();
if filtered_users.is_empty() {
let empty_msg = if app.dms_state.new_conversation_search_query.is_empty() {
"No mutual friends available for messaging"
} else {
"No users match your search"
};
render_empty_state(frame, chunks[1], empty_msg, &theme);
} else {
let list_config = UserListConfig {
selected_index: app.dms_state.new_conversation_selected_index,
show_stats: true,
};
render_user_list(frame, chunks[1], &filtered_users, &list_config, &theme);
}
let footer_text = if app.dms_state.new_conversation_search_mode {
"Type to search | Esc: Exit search"
} else {
"↑/↓/j/k: Navigate | Enter: Start Conversation | /: Search | Esc: Close"
};
render_footer(frame, chunks[2], footer_text, &theme);
}
pub fn render_dm_error_modal(frame: &mut Frame, app: &App, area: Rect) {
let theme = get_theme_colors(app);
let config = ModalConfig::new("Error")
.with_size(50, 30)
.with_border_color(theme.error);
let inner = render_modal_container(frame, area, &config, &theme);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0), Constraint::Length(2), ])
.split(inner);
let message = Paragraph::new(app.dms_state.dm_error_message.as_str())
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.style(Style::default().fg(theme.text));
frame.render_widget(message, chunks[0]);
render_footer(frame, chunks[1], "Enter: Add Friend | Esc: Cancel", &theme);
}
pub fn render_user_search_modal(frame: &mut Frame, app: &mut App, area: Rect) {
let theme = get_theme_colors(app);
let config = ModalConfig::new(" Search Users ").with_size(70, 80);
let inner = render_modal_container(frame, area, &config, &theme);
if app.user_search_state.loading {
render_loading_state(frame, inner, "Searching...", &theme);
return;
}
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(inner);
let search_config = SearchBarConfig {
query: &app.user_search_state.search_query,
is_active: true,
placeholder: "Type to search users...",
mode: SearchBarMode::Search,
};
render_search_bar(frame, chunks[0], &search_config, &theme);
let results = &app.user_search_state.search_results;
if results.is_empty() {
let empty_msg = if app.user_search_state.search_query.is_empty() {
"Start typing to search for users"
} else if app.user_search_state.search_query.len() < 2 {
"Type at least 2 characters to search"
} else {
"No users found matching your search"
};
render_empty_state(frame, chunks[1], empty_msg, &theme);
} else {
let list_config = UserListConfig {
selected_index: app.user_search_state.selected_index,
show_stats: false,
};
render_user_list(frame, chunks[1], results, &list_config, &theme);
}
render_footer(
frame,
chunks[2],
"↑/↓/j/k: Navigate | Enter: View Profile | d: Send DM | Esc: Close",
&theme,
);
}