Files
telegram-tui/src/ui/profile.rs
Mikhail Kilin ffd52d2384 refactor: complete Phase 13 deep architecture refactoring (etaps 3-7)
Split monolithic files into modular architecture:
- ui/messages.rs (893→365 lines): extract modals/, compose_bar.rs
- tdlib/messages.rs (836→3 files): split into messages/mod, convert, operations
- config/mod.rs (642→3 files): extract validation.rs, loader.rs
- Code duplication cleanup: shared components, ~220 lines removed
- Documentation: PROJECT_STRUCTURE.md rewrite, 16 files got //! docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:28:11 +03:00

288 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::app::App;
use crate::app::methods::modal::ModalMethods;
use crate::tdlib::TdClientTrait;
use crate::tdlib::ProfileInfo;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
/// Рендерит режим просмотра профиля
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>, profile: &ProfileInfo) {
// Проверяем, показывать ли модалку подтверждения
let confirmation_step = app.get_leave_group_confirmation_step();
if confirmation_step > 0 {
render_leave_confirmation_modal(f, area, confirmation_step);
return;
}
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(0), // Profile info
Constraint::Length(3), // Actions help
])
.split(area);
// Header
let header_text = format!("👤 ПРОФИЛЬ: {}", profile.title);
let header = Paragraph::new(header_text)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
f.render_widget(header, chunks[0]);
// Profile info
let mut lines: Vec<Line> = Vec::new();
// Тип чата
lines.push(Line::from(vec![
Span::styled("Тип: ", Style::default().fg(Color::Gray)),
Span::styled(&profile.chat_type, Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
// ID
lines.push(Line::from(vec![
Span::styled("ID: ", Style::default().fg(Color::Gray)),
Span::styled(format!("{}", profile.chat_id), Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
// Username
if let Some(username) = &profile.username {
lines.push(Line::from(vec![
Span::styled("Username: ", Style::default().fg(Color::Gray)),
Span::styled(username, Style::default().fg(Color::Cyan)),
]));
lines.push(Line::from(""));
}
// Phone number (только для личных чатов)
if let Some(phone) = &profile.phone_number {
lines.push(Line::from(vec![
Span::styled("Телефон: ", Style::default().fg(Color::Gray)),
Span::styled(phone, Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
}
// Online status (только для личных чатов)
if let Some(status) = &profile.online_status {
lines.push(Line::from(vec![
Span::styled("Статус: ", Style::default().fg(Color::Gray)),
Span::styled(status, Style::default().fg(Color::Green)),
]));
lines.push(Line::from(""));
}
// Bio (только для личных чатов)
if let Some(bio) = &profile.bio {
lines.push(Line::from(vec![Span::styled("О себе: ", Style::default().fg(Color::Gray))]));
// Разбиваем bio на строки если длинное
let bio_lines: Vec<&str> = bio.lines().collect();
for bio_line in bio_lines {
lines.push(Line::from(Span::styled(bio_line, Style::default().fg(Color::White))));
}
lines.push(Line::from(""));
}
// Member count (для групп/каналов)
if let Some(count) = profile.member_count {
lines.push(Line::from(vec![
Span::styled("Участников: ", Style::default().fg(Color::Gray)),
Span::styled(format!("{}", count), Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
}
// Description (для групп/каналов)
if let Some(desc) = &profile.description {
lines.push(Line::from(vec![Span::styled("Описание: ", Style::default().fg(Color::Gray))]));
let desc_lines: Vec<&str> = desc.lines().collect();
for desc_line in desc_lines {
lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White))));
}
lines.push(Line::from(""));
}
// Invite link (для групп/каналов)
if let Some(link) = &profile.invite_link {
lines.push(Line::from(vec![
Span::styled("Ссылка: ", Style::default().fg(Color::Gray)),
Span::styled(
link,
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::UNDERLINED),
),
]));
lines.push(Line::from(""));
}
// Разделитель
lines.push(Line::from("────────────────────────────────"));
lines.push(Line::from(""));
// Действия
lines.push(Line::from(Span::styled(
"Действия:",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
let actions = get_available_actions(profile);
for (idx, action) in actions.iter().enumerate() {
let is_selected = idx == app.get_selected_profile_action().unwrap_or(0);
let marker = if is_selected { "" } else { " " };
let style = if is_selected {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
lines.push(Line::from(vec![
Span::styled(marker, Style::default().fg(Color::Yellow)),
Span::styled(*action, style),
]));
}
let info_widget = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.scroll((0, 0));
f.render_widget(info_widget, chunks[1]);
// Help bar
let help_line = Line::from(vec![
Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"),
Span::raw(" "),
Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("выбрать"),
Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw("выход"),
]);
let help = Paragraph::new(help_line)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.alignment(Alignment::Center);
f.render_widget(help, chunks[2]);
}
/// Получить список доступных действий
fn get_available_actions(profile: &ProfileInfo) -> Vec<&'static str> {
let mut actions = vec![];
if profile.username.is_some() {
actions.push("Открыть в браузере");
}
actions.push("Скопировать ID");
if profile.is_group {
actions.push("Покинуть группу");
}
actions
}
/// Рендерит модалку подтверждения выхода из группы
fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
// Затемняем фон
let modal_area = centered_rect(60, 30, area);
let text = if step == 1 {
"Вы хотите выйти из группы?"
} else {
"Вы ТОЧНО хотите выйти из группы?!?!?"
};
let lines = vec![
Line::from(""),
Line::from(Span::styled(
text,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(
"y/н/Enter",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw(" — да "),
Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw(" — нет"),
]),
];
let modal = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red))
.title(" ⚠ ВНИМАНИЕ ")
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
)
.alignment(Alignment::Center);
f.render_widget(modal, modal_area);
}
/// Вспомогательная функция для центрирования прямоугольника
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}