This commit is contained in:
Mikhail Kilin
2026-01-30 15:07:13 +03:00
parent 126c7482af
commit 4deb0fbe00
32 changed files with 1049 additions and 697 deletions

View File

@@ -2,9 +2,9 @@ mod state;
pub use state::AppScreen; pub use state::AppScreen;
use ratatui::widgets::ListState;
use crate::tdlib::client::ChatInfo; use crate::tdlib::client::ChatInfo;
use crate::tdlib::TdClient; use crate::tdlib::TdClient;
use ratatui::widgets::ListState;
pub struct App { pub struct App {
pub config: crate::config::Config, pub config: crate::config::Config,
@@ -87,7 +87,6 @@ pub struct App {
pub selected_reaction_index: usize, pub selected_reaction_index: usize,
} }
impl App { impl App {
pub fn new(config: crate::config::Config) -> App { pub fn new(config: crate::config::Config) -> App {
let mut state = ListState::default(); let mut state = ListState::default();
@@ -226,13 +225,14 @@ impl App {
self.selected_message_index = Some( self.selected_message_index = Some(
self.selected_message_index self.selected_message_index
.map(|i| (i + 1).min(total - 1)) .map(|i| (i + 1).min(total - 1))
.unwrap_or(0) .unwrap_or(0),
); );
} }
/// Выбрать следующее сообщение (вниз по списку = уменьшить индекс) /// Выбрать следующее сообщение (вниз по списку = уменьшить индекс)
pub fn select_next_message(&mut self) { pub fn select_next_message(&mut self) {
self.selected_message_index = self.selected_message_index self.selected_message_index = self
.selected_message_index
.map(|i| if i > 0 { Some(i - 1) } else { None }) .map(|i| if i > 0 { Some(i - 1) } else { None })
.flatten(); .flatten();
} }
@@ -312,7 +312,8 @@ impl App {
pub fn get_filtered_chats(&self) -> Vec<&ChatInfo> { pub fn get_filtered_chats(&self) -> Vec<&ChatInfo> {
let folder_filtered: Vec<&ChatInfo> = match self.selected_folder_id { let folder_filtered: Vec<&ChatInfo> = match self.selected_folder_id {
None => self.chats.iter().collect(), // All - показываем все None => self.chats.iter().collect(), // All - показываем все
Some(folder_id) => self.chats Some(folder_id) => self
.chats
.iter() .iter()
.filter(|c| c.folder_ids.contains(&folder_id)) .filter(|c| c.folder_ids.contains(&folder_id))
.collect(), .collect(),
@@ -410,7 +411,10 @@ impl App {
/// Получить сообщение, на которое отвечаем /// Получить сообщение, на которое отвечаем
pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
self.replying_to_message_id.and_then(|id| { self.replying_to_message_id.and_then(|id| {
self.td_client.current_chat_messages.iter().find(|m| m.id == id) self.td_client
.current_chat_messages
.iter()
.find(|m| m.id == id)
}) })
} }
@@ -441,7 +445,10 @@ impl App {
/// Получить сообщение для пересылки /// Получить сообщение для пересылки
pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
self.forwarding_message_id.and_then(|id| { self.forwarding_message_id.and_then(|id| {
self.td_client.current_chat_messages.iter().find(|m| m.id == id) self.td_client
.current_chat_messages
.iter()
.find(|m| m.id == id)
}) })
} }
@@ -470,7 +477,9 @@ impl App {
/// Выбрать предыдущий pinned (вверх = более старый) /// Выбрать предыдущий pinned (вверх = более старый)
pub fn select_previous_pinned(&mut self) { pub fn select_previous_pinned(&mut self) {
if !self.pinned_messages.is_empty() && self.selected_pinned_index < self.pinned_messages.len() - 1 { if !self.pinned_messages.is_empty()
&& self.selected_pinned_index < self.pinned_messages.len() - 1
{
self.selected_pinned_index += 1; self.selected_pinned_index += 1;
} }
} }
@@ -530,8 +539,8 @@ impl App {
/// Выбрать следующий результат (вниз) /// Выбрать следующий результат (вниз)
pub fn select_next_search_result(&mut self) { pub fn select_next_search_result(&mut self) {
if !self.message_search_results.is_empty() if !self.message_search_results.is_empty()
&& self.selected_search_result_index < self.message_search_results.len() - 1 && self.selected_search_result_index < self.message_search_results.len() - 1
{ {
self.selected_search_result_index += 1; self.selected_search_result_index += 1;
} }
@@ -539,7 +548,8 @@ impl App {
/// Получить текущий выбранный результат /// Получить текущий выбранный результат
pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> { pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> {
self.message_search_results.get(self.selected_search_result_index) self.message_search_results
.get(self.selected_search_result_index)
} }
/// Получить ID выбранного результата для перехода /// Получить ID выбранного результата для перехода
@@ -629,7 +639,11 @@ impl App {
self.is_reaction_picker_mode self.is_reaction_picker_mode
} }
pub fn enter_reaction_picker_mode(&mut self, message_id: i64, available_reactions: Vec<String>) { pub fn enter_reaction_picker_mode(
&mut self,
message_id: i64,
available_reactions: Vec<String>,
) {
self.is_reaction_picker_mode = true; self.is_reaction_picker_mode = true;
self.selected_message_for_reaction = Some(message_id); self.selected_message_for_reaction = Some(message_id);
self.available_reactions = available_reactions; self.available_reactions = available_reactions;

View File

@@ -67,9 +67,7 @@ fn default_reaction_other_color() -> String {
impl Default for GeneralConfig { impl Default for GeneralConfig {
fn default() -> Self { fn default() -> Self {
Self { Self { timezone: default_timezone() }
timezone: default_timezone(),
}
} }
} }
@@ -132,15 +130,13 @@ impl Config {
} }
match fs::read_to_string(&config_path) { match fs::read_to_string(&config_path) {
Ok(content) => { Ok(content) => match toml::from_str::<Config>(&content) {
match toml::from_str::<Config>(&content) { Ok(config) => config,
Ok(config) => config, Err(e) => {
Err(e) => { eprintln!("Warning: Could not parse config file: {}", e);
eprintln!("Warning: Could not parse config file: {}", e); Self::default()
Self::default()
}
} }
} },
Err(e) => { Err(e) => {
eprintln!("Warning: Could not read config file: {}", e); eprintln!("Warning: Could not read config file: {}", e);
Self::default() Self::default()
@@ -150,8 +146,8 @@ impl Config {
/// Сохранить конфигурацию в файл /// Сохранить конфигурацию в файл
pub fn save(&self) -> Result<(), String> { pub fn save(&self) -> Result<(), String> {
let config_dir = Self::config_dir() let config_dir =
.ok_or_else(|| "Could not determine config directory".to_string())?; Self::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
// Создаём директорию если её нет // Создаём директорию если её нет
fs::create_dir_all(&config_dir) fs::create_dir_all(&config_dir)

View File

@@ -1,8 +1,8 @@
use crate::app::App;
use crate::tdlib::client::AuthState;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use std::time::Duration; use std::time::Duration;
use tokio::time::timeout; use tokio::time::timeout;
use crate::app::App;
use crate::tdlib::client::AuthState;
pub async fn handle(app: &mut App, key_code: KeyCode) { pub async fn handle(app: &mut App, key_code: KeyCode) {
match &app.td_client.auth_state { match &app.td_client.auth_state {
@@ -18,7 +18,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => { KeyCode::Enter => {
if !app.phone_input.is_empty() { if !app.phone_input.is_empty() {
app.status_message = Some("Отправка номера...".to_string()); app.status_message = Some("Отправка номера...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_phone_number(app.phone_input.clone())).await { match timeout(
Duration::from_secs(10),
app.td_client.send_phone_number(app.phone_input.clone()),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
app.error_message = None; app.error_message = None;
app.status_message = None; app.status_message = None;
@@ -48,7 +53,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => { KeyCode::Enter => {
if !app.code_input.is_empty() { if !app.code_input.is_empty() {
app.status_message = Some("Проверка кода...".to_string()); app.status_message = Some("Проверка кода...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_code(app.code_input.clone())).await { match timeout(
Duration::from_secs(10),
app.td_client.send_code(app.code_input.clone()),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
app.error_message = None; app.error_message = None;
app.status_message = None; app.status_message = None;
@@ -78,7 +88,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => { KeyCode::Enter => {
if !app.password_input.is_empty() { if !app.password_input.is_empty() {
app.status_message = Some("Проверка пароля...".to_string()); app.status_message = Some("Проверка пароля...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_password(app.password_input.clone())).await { match timeout(
Duration::from_secs(10),
app.td_client.send_password(app.password_input.clone()),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
app.error_message = None; app.error_message = None;
app.status_message = None; app.status_message = None;

View File

@@ -1,8 +1,8 @@
use crate::app::App;
use crate::tdlib::ChatAction;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::timeout; use tokio::time::timeout;
use crate::app::App;
use crate::tdlib::ChatAction;
pub async fn handle(app: &mut App, key: KeyEvent) { pub async fn handle(app: &mut App, key: KeyEvent) {
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
@@ -27,7 +27,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if app.selected_chat_id.is_some() && !app.is_pinned_mode() { if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
app.status_message = Some("Загрузка закреплённых...".to_string()); app.status_message = Some("Загрузка закреплённых...".to_string());
match timeout(Duration::from_secs(5), app.td_client.get_pinned_messages(chat_id)).await { match timeout(
Duration::from_secs(5),
app.td_client.get_pinned_messages(chat_id),
)
.await
{
Ok(Ok(messages)) => { Ok(Ok(messages)) => {
if messages.is_empty() { if messages.is_empty() {
app.status_message = Some("Нет закреплённых сообщений".to_string()); app.status_message = Some("Нет закреплённых сообщений".to_string());
@@ -51,7 +56,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
} }
KeyCode::Char('f') if has_ctrl => { KeyCode::Char('f') if has_ctrl => {
// Ctrl+F - поиск по сообщениям в открытом чате // Ctrl+F - поиск по сообщениям в открытом чате
if app.selected_chat_id.is_some() && !app.is_pinned_mode() && !app.is_message_search_mode() { if app.selected_chat_id.is_some()
&& !app.is_pinned_mode()
&& !app.is_message_search_mode()
{
app.enter_message_search_mode(); app.enter_message_search_mode();
} }
return; return;
@@ -125,13 +133,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if profile.username.is_some() { if profile.username.is_some() {
if action_index == current_idx { if action_index == current_idx {
if let Some(username) = &profile.username { if let Some(username) = &profile.username {
let url = format!("https://t.me/{}", username.trim_start_matches('@')); let url = format!(
"https://t.me/{}",
username.trim_start_matches('@')
);
match open::that(&url) { match open::that(&url) {
Ok(_) => { Ok(_) => {
app.status_message = Some(format!("Открыто: {}", url)); app.status_message = Some(format!("Открыто: {}", url));
} }
Err(e) => { Err(e) => {
app.error_message = Some(format!("Ошибка открытия браузера: {}", e)); app.error_message =
Some(format!("Ошибка открытия браузера: {}", e));
} }
} }
} }
@@ -142,7 +154,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Действие: Скопировать ID // Действие: Скопировать ID
if action_index == current_idx { if action_index == current_idx {
app.status_message = Some(format!("ID скопирован: {}", profile.chat_id)); app.status_message =
Some(format!("ID скопирован: {}", profile.chat_id));
return; return;
} }
current_idx += 1; current_idx += 1;
@@ -174,10 +187,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
KeyCode::Enter => { KeyCode::Enter => {
// Перейти к выбранному сообщению // Перейти к выбранному сообщению
if let Some(msg_id) = app.get_selected_search_result_id() { if let Some(msg_id) = app.get_selected_search_result_id() {
let msg_index = app.td_client.current_chat_messages let msg_index = app
.td_client
.current_chat_messages
.iter() .iter()
.position(|m| m.id == msg_id); .position(|m| m.id == msg_id);
if let Some(idx) = msg_index { if let Some(idx) = msg_index {
let total = app.td_client.current_chat_messages.len(); let total = app.td_client.current_chat_messages.len();
app.message_scroll_offset = total.saturating_sub(idx + 5); app.message_scroll_offset = total.saturating_sub(idx + 5);
@@ -192,8 +207,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if !app.message_search_query.is_empty() { if !app.message_search_query.is_empty() {
if let Ok(Ok(results)) = timeout( if let Ok(Ok(results)) = timeout(
Duration::from_secs(3), Duration::from_secs(3),
app.td_client.search_messages(chat_id, &app.message_search_query) app.td_client
).await { .search_messages(chat_id, &app.message_search_query),
)
.await
{
app.set_search_results(results); app.set_search_results(results);
} }
} else { } else {
@@ -207,8 +225,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
if let Ok(Ok(results)) = timeout( if let Ok(Ok(results)) = timeout(
Duration::from_secs(3), Duration::from_secs(3),
app.td_client.search_messages(chat_id, &app.message_search_query) app.td_client
).await { .search_messages(chat_id, &app.message_search_query),
)
.await
{
app.set_search_results(results); app.set_search_results(results);
} }
} }
@@ -234,10 +255,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Перейти к сообщению в истории // Перейти к сообщению в истории
if let Some(msg_id) = app.get_selected_pinned_id() { if let Some(msg_id) = app.get_selected_pinned_id() {
// Ищем индекс сообщения в текущей истории // Ищем индекс сообщения в текущей истории
let msg_index = app.td_client.current_chat_messages let msg_index = app
.td_client
.current_chat_messages
.iter() .iter()
.position(|m| m.id == msg_id); .position(|m| m.id == msg_id);
if let Some(idx) = msg_index { if let Some(idx) = msg_index {
// Вычисляем scroll offset чтобы показать сообщение // Вычисляем scroll offset чтобы показать сообщение
let total = app.td_client.current_chat_messages.len(); let total = app.td_client.current_chat_messages.len();
@@ -284,13 +307,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(chat_id) = app.selected_chat_id { if let Some(chat_id) = app.selected_chat_id {
app.status_message = Some("Отправка реакции...".to_string()); app.status_message = Some("Отправка реакции...".to_string());
app.needs_redraw = true; app.needs_redraw = true;
match timeout( match timeout(
Duration::from_secs(5), Duration::from_secs(5),
app.td_client.toggle_reaction(chat_id, message_id, emoji.clone()) app.td_client
).await { .toggle_reaction(chat_id, message_id, emoji.clone()),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
app.status_message = Some(format!("Реакция {} добавлена", emoji)); app.status_message =
Some(format!("Реакция {} добавлена", emoji));
app.exit_reaction_picker_mode(); app.exit_reaction_picker_mode();
app.needs_redraw = true; app.needs_redraw = true;
} }
@@ -300,7 +327,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.needs_redraw = true; app.needs_redraw = true;
} }
Err(_) => { Err(_) => {
app.error_message = Some("Таймаут отправки реакции".to_string()); app.error_message =
Some("Таймаут отправки реакции".to_string());
app.status_message = None; app.status_message = None;
app.needs_redraw = true; app.needs_redraw = true;
} }
@@ -326,7 +354,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(msg_id) = app.confirm_delete_message_id { if let Some(msg_id) = app.confirm_delete_message_id {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
// Находим сообщение для проверки can_be_deleted_for_all_users // Находим сообщение для проверки can_be_deleted_for_all_users
let can_delete_for_all = app.td_client.current_chat_messages let can_delete_for_all = app
.td_client
.current_chat_messages
.iter() .iter()
.find(|m| m.id == msg_id) .find(|m| m.id == msg_id)
.map(|m| m.can_be_deleted_for_all_users) .map(|m| m.can_be_deleted_for_all_users)
@@ -334,11 +364,19 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
match timeout( match timeout(
Duration::from_secs(5), Duration::from_secs(5),
app.td_client.delete_messages(chat_id, vec![msg_id], can_delete_for_all) app.td_client.delete_messages(
).await { chat_id,
vec![msg_id],
can_delete_for_all,
),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
// Удаляем из локального списка // Удаляем из локального списка
app.td_client.current_chat_messages.retain(|m| m.id != msg_id); app.td_client
.current_chat_messages
.retain(|m| m.id != msg_id);
app.selected_message_index = None; app.selected_message_index = None;
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -377,10 +415,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(from_chat_id) = app.get_selected_chat_id() { if let Some(from_chat_id) = app.get_selected_chat_id() {
match timeout( match timeout(
Duration::from_secs(5), Duration::from_secs(5),
app.td_client.forward_messages(to_chat_id, from_chat_id, vec![msg_id]) app.td_client.forward_messages(
).await { to_chat_id,
from_chat_id,
vec![msg_id],
),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
app.status_message = Some("Сообщение переслано".to_string()); app.status_message =
Some("Сообщение переслано".to_string());
} }
Ok(Err(e)) => { Ok(Err(e)) => {
app.error_message = Some(e); app.error_message = Some(e);
@@ -418,12 +463,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
app.status_message = Some("Загрузка сообщений...".to_string()); app.status_message = Some("Загрузка сообщений...".to_string());
app.message_scroll_offset = 0; app.message_scroll_offset = 0;
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await { match timeout(
Duration::from_secs(10),
app.td_client.get_chat_history(chat_id, 100),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
// Загружаем недостающие reply info // Загружаем недостающие reply info
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await; let _ = timeout(
Duration::from_secs(5),
app.td_client.fetch_missing_reply_info(),
)
.await;
// Загружаем последнее закреплённое сообщение // Загружаем последнее закреплённое сообщение
let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await; let _ = timeout(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Загружаем черновик // Загружаем черновик
app.load_draft(); app.load_draft();
app.status_message = None; app.status_message = None;
@@ -460,8 +518,6 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
return; return;
} }
// Enter - открыть чат, отправить сообщение или редактировать // Enter - открыть чат, отправить сообщение или редактировать
if key.code == KeyCode::Enter { if key.code == KeyCode::Enter {
if app.selected_chat_id.is_some() { if app.selected_chat_id.is_some() {
@@ -488,10 +544,20 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.cursor_position = 0; app.cursor_position = 0;
app.editing_message_id = None; app.editing_message_id = None;
match timeout(Duration::from_secs(5), app.td_client.edit_message(chat_id, msg_id, text)).await { match timeout(
Duration::from_secs(5),
app.td_client.edit_message(chat_id, msg_id, text),
)
.await
{
Ok(Ok(edited_msg)) => { Ok(Ok(edited_msg)) => {
// Обновляем сообщение в списке // Обновляем сообщение в списке
if let Some(msg) = app.td_client.current_chat_messages.iter_mut().find(|m| m.id == msg_id) { if let Some(msg) = app
.td_client
.current_chat_messages
.iter_mut()
.find(|m| m.id == msg_id)
{
msg.content = edited_msg.content; msg.content = edited_msg.content;
msg.entities = edited_msg.entities; msg.entities = edited_msg.entities;
msg.edit_date = edited_msg.edit_date; msg.edit_date = edited_msg.edit_date;
@@ -521,9 +587,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.last_typing_sent = None; app.last_typing_sent = None;
// Отменяем typing status // Отменяем typing status
app.td_client.send_chat_action(chat_id, ChatAction::Cancel).await; app.td_client
.send_chat_action(chat_id, ChatAction::Cancel)
.await;
match timeout(Duration::from_secs(5), app.td_client.send_message(chat_id, text, reply_to_id, reply_info)).await { match timeout(
Duration::from_secs(5),
app.td_client
.send_message(chat_id, text, reply_to_id, reply_info),
)
.await
{
Ok(Ok(sent_msg)) => { Ok(Ok(sent_msg)) => {
// Добавляем отправленное сообщение в список (с лимитом) // Добавляем отправленное сообщение в список (с лимитом)
app.td_client.push_message(sent_msg); app.td_client.push_message(sent_msg);
@@ -549,12 +623,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
app.status_message = Some("Загрузка сообщений...".to_string()); app.status_message = Some("Загрузка сообщений...".to_string());
app.message_scroll_offset = 0; app.message_scroll_offset = 0;
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await { match timeout(
Duration::from_secs(10),
app.td_client.get_chat_history(chat_id, 100),
)
.await
{
Ok(Ok(_)) => { Ok(Ok(_)) => {
// Загружаем недостающие reply info // Загружаем недостающие reply info
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await; let _ = timeout(
Duration::from_secs(5),
app.td_client.fetch_missing_reply_info(),
)
.await;
// Загружаем последнее закреплённое сообщение // Загружаем последнее закреплённое сообщение
let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await; let _ = timeout(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Загружаем черновик // Загружаем черновик
app.load_draft(); app.load_draft();
app.status_message = None; app.status_message = None;
@@ -593,7 +680,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
let _ = app.td_client.set_draft_message(chat_id, draft_text).await; let _ = app.td_client.set_draft_message(chat_id, draft_text).await;
} else if app.message_input.is_empty() { } else if app.message_input.is_empty() {
// Очищаем черновик если инпут пустой // Очищаем черновик если инпут пустой
let _ = app.td_client.set_draft_message(chat_id, String::new()).await; let _ = app
.td_client
.set_draft_message(chat_id, String::new())
.await;
} }
} }
app.close_chat(); app.close_chat();
@@ -616,7 +706,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => { KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => {
// Показать модалку подтверждения удаления // Показать модалку подтверждения удаления
if let Some(msg) = app.get_selected_message() { if let Some(msg) = app.get_selected_message() {
let can_delete = msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users; let can_delete =
msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users;
if can_delete { if can_delete {
app.confirm_delete_message_id = Some(msg.id); app.confirm_delete_message_id = Some(msg.id);
} }
@@ -649,18 +740,22 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(msg) = app.get_selected_message() { if let Some(msg) = app.get_selected_message() {
let chat_id = app.selected_chat_id.unwrap(); let chat_id = app.selected_chat_id.unwrap();
let message_id = msg.id; let message_id = msg.id;
app.status_message = Some("Загрузка реакций...".to_string()); app.status_message = Some("Загрузка реакций...".to_string());
app.needs_redraw = true; app.needs_redraw = true;
// Запрашиваем доступные реакции // Запрашиваем доступные реакции
match timeout( match timeout(
Duration::from_secs(5), Duration::from_secs(5),
app.td_client.get_message_available_reactions(chat_id, message_id) app.td_client
).await { .get_message_available_reactions(chat_id, message_id),
)
.await
{
Ok(Ok(reactions)) => { Ok(Ok(reactions)) => {
if reactions.is_empty() { if reactions.is_empty() {
app.error_message = Some("Реакции недоступны для этого сообщения".to_string()); app.error_message =
Some("Реакции недоступны для этого сообщения".to_string());
app.status_message = None; app.status_message = None;
app.needs_redraw = true; app.needs_redraw = true;
} else { } else {
@@ -691,7 +786,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if key.code == KeyCode::Char('u') && has_ctrl { if key.code == KeyCode::Char('u') && has_ctrl {
if let Some(chat_id) = app.selected_chat_id { if let Some(chat_id) = app.selected_chat_id {
app.status_message = Some("Загрузка профиля...".to_string()); app.status_message = Some("Загрузка профиля...".to_string());
match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await { match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await
{
Ok(Ok(profile)) => { Ok(Ok(profile)) => {
app.profile_info = Some(profile); app.profile_info = Some(profile);
app.enter_profile_mode(); app.enter_profile_mode();
@@ -756,12 +852,15 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.cursor_position += 1; app.cursor_position += 1;
// Отправляем typing status с throttling (не чаще 1 раза в 5 сек) // Отправляем typing status с throttling (не чаще 1 раза в 5 сек)
let should_send_typing = app.last_typing_sent let should_send_typing = app
.last_typing_sent
.map(|t| t.elapsed().as_secs() >= 5) .map(|t| t.elapsed().as_secs() >= 5)
.unwrap_or(true); .unwrap_or(true);
if should_send_typing { if should_send_typing {
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
app.td_client.send_chat_action(chat_id, ChatAction::Typing).await; app.td_client
.send_chat_action(chat_id, ChatAction::Typing)
.await;
app.last_typing_sent = Some(Instant::now()); app.last_typing_sent = Some(Instant::now());
} }
} }
@@ -804,18 +903,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Проверяем, нужно ли подгрузить старые сообщения // Проверяем, нужно ли подгрузить старые сообщения
if !app.td_client.current_chat_messages.is_empty() { if !app.td_client.current_chat_messages.is_empty() {
let oldest_msg_id = app.td_client.current_chat_messages.first().map(|m| m.id).unwrap_or(0); let oldest_msg_id = app
.td_client
.current_chat_messages
.first()
.map(|m| m.id)
.unwrap_or(0);
if let Some(chat_id) = app.get_selected_chat_id() { if let Some(chat_id) = app.get_selected_chat_id() {
// Подгружаем больше сообщений если скролл близко к верху // Подгружаем больше сообщений если скролл близко к верху
if app.message_scroll_offset > app.td_client.current_chat_messages.len().saturating_sub(10) { if app.message_scroll_offset
> app.td_client.current_chat_messages.len().saturating_sub(10)
{
if let Ok(Ok(older)) = timeout( if let Ok(Ok(older)) = timeout(
Duration::from_secs(3), Duration::from_secs(3),
app.td_client.load_older_messages(chat_id, oldest_msg_id, 20) app.td_client
).await { .load_older_messages(chat_id, oldest_msg_id, 20),
)
.await
{
if !older.is_empty() { if !older.is_empty() {
// Добавляем старые сообщения в начало // Добавляем старые сообщения в начало
let mut new_messages = older; let mut new_messages = older;
new_messages.extend(app.td_client.current_chat_messages.drain(..)); new_messages
.extend(app.td_client.current_chat_messages.drain(..));
app.td_client.current_chat_messages = new_messages; app.td_client.current_chat_messages = new_messages;
} }
} }
@@ -848,7 +958,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.selected_folder_id = Some(folder_id); app.selected_folder_id = Some(folder_id);
// Загружаем чаты папки // Загружаем чаты папки
app.status_message = Some("Загрузка чатов папки...".to_string()); app.status_message = Some("Загрузка чатов папки...".to_string());
let _ = timeout(Duration::from_secs(5), app.td_client.load_folder_chats(folder_id, 50)).await; let _ = timeout(
Duration::from_secs(5),
app.td_client.load_folder_chats(folder_id, 50),
)
.await;
app.status_message = None; app.status_message = None;
} }
} }
@@ -862,73 +976,76 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
/// Подсчёт количества доступных действий в профиле /// Подсчёт количества доступных действий в профиле
fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize { fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
let mut count = 0; let mut count = 0;
if profile.username.is_some() { if profile.username.is_some() {
count += 1; // Открыть в браузере count += 1; // Открыть в браузере
} }
count += 1; // Скопировать ID count += 1; // Скопировать ID
if profile.is_group { if profile.is_group {
count += 1; // Покинуть группу count += 1; // Покинуть группу
} }
count count
} }
/// Копирует текст в системный буфер обмена /// Копирует текст в системный буфер обмена
fn copy_to_clipboard(text: &str) -> Result<(), String> { fn copy_to_clipboard(text: &str) -> Result<(), String> {
use arboard::Clipboard; use arboard::Clipboard;
let mut clipboard = Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?; let mut clipboard =
clipboard.set_text(text).map_err(|e| format!("Не удалось скопировать: {}", e))?; Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
clipboard
.set_text(text)
.map_err(|e| format!("Не удалось скопировать: {}", e))?;
Ok(()) Ok(())
} }
/// Форматирует сообщение для копирования с контекстом /// Форматирует сообщение для копирования с контекстом
fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String { fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String {
let mut result = String::new(); let mut result = String::new();
// Добавляем forward контекст если есть // Добавляем forward контекст если есть
if let Some(forward) = &msg.forward_from { if let Some(forward) = &msg.forward_from {
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name)); result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
} }
// Добавляем reply контекст если есть // Добавляем reply контекст если есть
if let Some(reply) = &msg.reply_to { if let Some(reply) = &msg.reply_to {
result.push_str(&format!("{}: {}\n", reply.sender_name, reply.text)); result.push_str(&format!("{}: {}\n", reply.sender_name, reply.text));
} }
// Добавляем основной текст с markdown форматированием // Добавляем основной текст с markdown форматированием
result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities)); result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities));
result result
} }
/// Конвертирует текст с entities в markdown /// Конвертирует текст с entities в markdown
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String { fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
use tdlib_rs::enums::TextEntityType; use tdlib_rs::enums::TextEntityType;
if entities.is_empty() { if entities.is_empty() {
return text.to_string(); return text.to_string();
} }
// Создаём вектор символов для работы с unicode // Создаём вектор символов для работы с unicode
let chars: Vec<char> = text.chars().collect(); let chars: Vec<char> = text.chars().collect();
let mut result = String::new(); let mut result = String::new();
let mut i = 0; let mut i = 0;
while i < chars.len() { while i < chars.len() {
// Ищем entity, который начинается в текущей позиции // Ищем entity, который начинается в текущей позиции
let mut entity_found = false; let mut entity_found = false;
for entity in entities { for entity in entities {
if entity.offset as usize == i { if entity.offset as usize == i {
entity_found = true; entity_found = true;
let end = (entity.offset + entity.length) as usize; let end = (entity.offset + entity.length) as usize;
let entity_text: String = chars[i..end.min(chars.len())].iter().collect(); let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
// Применяем форматирование в зависимости от типа // Применяем форматирование в зависимости от типа
let formatted = match &entity.r#type { let formatted = match &entity.r#type {
TextEntityType::Bold => format!("**{}**", entity_text), TextEntityType::Bold => format!("**{}**", entity_text),
@@ -948,18 +1065,18 @@ fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEnt
TextEntityType::Spoiler => format!("||{}||", entity_text), TextEntityType::Spoiler => format!("||{}||", entity_text),
_ => entity_text, _ => entity_text,
}; };
result.push_str(&formatted); result.push_str(&formatted);
i = end; i = end;
break; break;
} }
} }
if !entity_found { if !entity_found {
result.push(chars[i]); result.push(chars[i]);
i += 1; i += 1;
} }
} }
result result
} }

View File

@@ -46,11 +46,7 @@ async fn main() -> Result<(), io::Error> {
// Restore terminal // Restore terminal
disable_raw_mode()?; disable_raw_mode()?;
execute!( execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?; terminal.show_cursor()?;
if let Err(err) = res { if let Err(err) = res {
@@ -91,20 +87,20 @@ async fn run_app<B: ratatui::backend::Backend>(
tokio::spawn(async move { tokio::spawn(async move {
let _ = tdlib_rs::functions::set_tdlib_parameters( let _ = tdlib_rs::functions::set_tdlib_parameters(
false, // use_test_dc false, // use_test_dc
"tdlib_data".to_string(), // database_directory "tdlib_data".to_string(), // database_directory
"".to_string(), // files_directory "".to_string(), // files_directory
"".to_string(), // database_encryption_key "".to_string(), // database_encryption_key
true, // use_file_database true, // use_file_database
true, // use_chat_info_database true, // use_chat_info_database
true, // use_message_database true, // use_message_database
false, // use_secret_chats false, // use_secret_chats
api_id, api_id,
api_hash, api_hash,
"en".to_string(), // system_language_code "en".to_string(), // system_language_code
"Desktop".to_string(), // device_model "Desktop".to_string(), // device_model
"".to_string(), // system_version "".to_string(), // system_version
env!("CARGO_PKG_VERSION").to_string(), // application_version env!("CARGO_PKG_VERSION").to_string(), // application_version
client_id, client_id,
) )
.await; .await;
@@ -156,7 +152,9 @@ async fn run_app<B: ratatui::backend::Backend>(
match event::read()? { match event::read()? {
Event::Key(key) => { Event::Key(key) => {
// Global quit command // Global quit command
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { if key.code == KeyCode::Char('c')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
// Graceful shutdown // Graceful shutdown
should_stop.store(true, Ordering::Relaxed); should_stop.store(true, Ordering::Relaxed);
@@ -164,10 +162,7 @@ async fn run_app<B: ratatui::backend::Backend>(
let _ = tdlib_rs::functions::close(app.td_client.client_id()).await; let _ = tdlib_rs::functions::close(app.td_client.client_id()).await;
// Ждём завершения polling задачи (с таймаутом) // Ждём завершения polling задачи (с таймаутом)
let _ = tokio::time::timeout( let _ = tokio::time::timeout(Duration::from_secs(2), polling_handle).await;
Duration::from_secs(2),
polling_handle
).await;
return Ok(()); return Ok(());
} }

View File

@@ -1,7 +1,10 @@
use std::env;
use std::collections::HashMap; use std::collections::HashMap;
use std::env;
use std::time::Instant; use std::time::Instant;
use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, SearchMessagesFilter, Update, User, UserStatus}; use tdlib_rs::enums::{
AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent,
MessageSender, SearchMessagesFilter, Update, User, UserStatus,
};
use tdlib_rs::types::TextEntity; use tdlib_rs::types::TextEntity;
/// Максимальный размер кэшей пользователей /// Максимальный размер кэшей пользователей
@@ -311,7 +314,11 @@ impl TdClient {
/// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to) /// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to)
pub fn push_message(&mut self, msg: MessageInfo) { pub fn push_message(&mut self, msg: MessageInfo) {
// Проверяем, есть ли уже сообщение с таким id // Проверяем, есть ли уже сообщение с таким id
if let Some(idx) = self.current_chat_messages.iter().position(|m| m.id == msg.id) { if let Some(idx) = self
.current_chat_messages
.iter()
.position(|m| m.id == msg.id)
{
// Если новое сообщение имеет reply_to, или старое не имеет — заменяем // Если новое сообщение имеет reply_to, или старое не имеет — заменяем
if msg.reply_to.is_some() || self.current_chat_messages[idx].reply_to.is_none() { if msg.reply_to.is_some() || self.current_chat_messages[idx].reply_to.is_none() {
self.current_chat_messages[idx] = msg; self.current_chat_messages[idx] = msg;
@@ -350,7 +357,8 @@ impl TdClient {
/// Например: "Вася печатает..." /// Например: "Вася печатает..."
pub fn get_typing_text(&self) -> Option<String> { pub fn get_typing_text(&self) -> Option<String> {
self.typing_status.as_ref().map(|(user_id, action, _)| { self.typing_status.as_ref().map(|(user_id, action, _)| {
let name = self.user_names let name = self
.user_names
.peek(user_id) .peek(user_id)
.cloned() .cloned()
.unwrap_or_else(|| "Кто-то".to_string()); .unwrap_or_else(|| "Кто-то".to_string());
@@ -361,20 +369,20 @@ impl TdClient {
/// Инициализация TDLib с параметрами /// Инициализация TDLib с параметрами
pub async fn init(&mut self) -> Result<(), String> { pub async fn init(&mut self) -> Result<(), String> {
let result = functions::set_tdlib_parameters( let result = functions::set_tdlib_parameters(
false, // use_test_dc false, // use_test_dc
"tdlib_data".to_string(), // database_directory "tdlib_data".to_string(), // database_directory
"".to_string(), // files_directory "".to_string(), // files_directory
"".to_string(), // database_encryption_key "".to_string(), // database_encryption_key
true, // use_file_database true, // use_file_database
true, // use_chat_info_database true, // use_chat_info_database
true, // use_message_database true, // use_message_database
false, // use_secret_chats false, // use_secret_chats
self.api_id, // api_id self.api_id, // api_id
self.api_hash.clone(), // api_hash self.api_hash.clone(), // api_hash
"en".to_string(), // system_language_code "en".to_string(), // system_language_code
"Desktop".to_string(), // device_model "Desktop".to_string(), // device_model
"".to_string(), // system_version "".to_string(), // system_version
env!("CARGO_PKG_VERSION").to_string(), // application_version env!("CARGO_PKG_VERSION").to_string(), // application_version
self.client_id, self.client_id,
) )
.await; .await;
@@ -457,7 +465,9 @@ impl TdClient {
if update.position.order == 0 { if update.position.order == 0 {
// Чат больше не в Main (перемещён в архив и т.д.) // Чат больше не в Main (перемещён в архив и т.д.)
self.chats.retain(|c| c.id != update.chat_id); self.chats.retain(|c| c.id != update.chat_id);
} else if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { } else if let Some(chat) =
self.chats.iter_mut().find(|c| c.id == update.chat_id)
{
// Обновляем позицию существующего чата // Обновляем позицию существующего чата
chat.order = update.position.order; chat.order = update.position.order;
chat.is_pinned = update.position.is_pinned; chat.is_pinned = update.position.is_pinned;
@@ -493,7 +503,10 @@ impl TdClient {
let is_incoming = !msg_info.is_outgoing; let is_incoming = !msg_info.is_outgoing;
// Проверяем, есть ли уже сообщение с таким id // Проверяем, есть ли уже сообщение с таким id
let existing_idx = self.current_chat_messages.iter().position(|m| m.id == msg_info.id); let existing_idx = self
.current_chat_messages
.iter()
.position(|m| m.id == msg_info.id);
match existing_idx { match existing_idx {
Some(idx) => { Some(idx) => {
@@ -505,8 +518,10 @@ impl TdClient {
// но сохраняем reply_to (добавленный при отправке) // но сохраняем reply_to (добавленный при отправке)
let existing = &mut self.current_chat_messages[idx]; let existing = &mut self.current_chat_messages[idx];
existing.can_be_edited = msg_info.can_be_edited; existing.can_be_edited = msg_info.can_be_edited;
existing.can_be_deleted_only_for_self = msg_info.can_be_deleted_only_for_self; existing.can_be_deleted_only_for_self =
existing.can_be_deleted_for_all_users = msg_info.can_be_deleted_for_all_users; msg_info.can_be_deleted_only_for_self;
existing.can_be_deleted_for_all_users =
msg_info.can_be_deleted_for_all_users;
existing.is_read = msg_info.is_read; existing.is_read = msg_info.is_read;
} }
} }
@@ -529,9 +544,8 @@ impl TdClient {
if user.first_name.is_empty() && user.last_name.is_empty() { if user.first_name.is_empty() && user.last_name.is_empty() {
// Удаляем чаты с этим пользователем из списка // Удаляем чаты с этим пользователем из списка
let user_id = user.id; let user_id = user.id;
self.chats.retain(|c| { self.chats
self.chat_user_ids.get(&c.id) != Some(&user_id) .retain(|c| self.chat_user_ids.get(&c.id) != Some(&user_id));
});
return; return;
} }
@@ -550,7 +564,8 @@ impl TdClient {
// Обновляем username в чатах, связанных с этим пользователем // Обновляем username в чатах, связанных с этим пользователем
for (&chat_id, &user_id) in &self.chat_user_ids.clone() { for (&chat_id, &user_id) in &self.chat_user_ids.clone() {
if user_id == user.id { if user_id == user.id {
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id)
{
chat.username = Some(format!("@{}", username)); chat.username = Some(format!("@{}", username));
} }
} }
@@ -564,10 +579,7 @@ impl TdClient {
self.folders = update self.folders = update
.chat_folders .chat_folders
.into_iter() .into_iter()
.map(|f| FolderInfo { .map(|f| FolderInfo { id: f.id, name: f.title })
id: f.id,
name: f.title,
})
.collect(); .collect();
self.main_chat_list_position = update.main_chat_list_position; self.main_chat_list_position = update.main_chat_list_position;
} }
@@ -607,14 +619,26 @@ impl TdClient {
let action_text = match update.action { let action_text = match update.action {
ChatAction::Typing => Some("печатает...".to_string()), ChatAction::Typing => Some("печатает...".to_string()),
ChatAction::RecordingVideo => Some("записывает видео...".to_string()), ChatAction::RecordingVideo => Some("записывает видео...".to_string()),
ChatAction::UploadingVideo(_) => Some("отправляет видео...".to_string()), ChatAction::UploadingVideo(_) => {
ChatAction::RecordingVoiceNote => Some("записывает голосовое...".to_string()), Some("отправляет видео...".to_string())
ChatAction::UploadingVoiceNote(_) => Some("отправляет голосовое...".to_string()), }
ChatAction::RecordingVoiceNote => {
Some("записывает голосовое...".to_string())
}
ChatAction::UploadingVoiceNote(_) => {
Some("отправляет голосовое...".to_string())
}
ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()), ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()),
ChatAction::UploadingDocument(_) => Some("отправляет файл...".to_string()), ChatAction::UploadingDocument(_) => {
Some("отправляет файл...".to_string())
}
ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()), ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()),
ChatAction::RecordingVideoNote => Some("записывает видеосообщение...".to_string()), ChatAction::RecordingVideoNote => {
ChatAction::UploadingVideoNote(_) => Some("отправляет видеосообщение...".to_string()), Some("записывает видеосообщение...".to_string())
}
ChatAction::UploadingVideoNote(_) => {
Some("отправляет видеосообщение...".to_string())
}
ChatAction::Cancel => None, // Отмена — сбрасываем статус ChatAction::Cancel => None, // Отмена — сбрасываем статус
_ => None, _ => None,
}; };
@@ -633,7 +657,9 @@ impl TdClient {
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
chat.draft_text = update.draft_message.as_ref().and_then(|draft| { chat.draft_text = update.draft_message.as_ref().and_then(|draft| {
// Извлекаем текст из InputMessageText // Извлекаем текст из InputMessageText
if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = &draft.input_message_text { if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) =
&draft.input_message_text
{
Some(text_msg.text.text.clone()) Some(text_msg.text.text.clone())
} else { } else {
None None
@@ -644,7 +670,11 @@ impl TdClient {
Update::MessageInteractionInfo(update) => { Update::MessageInteractionInfo(update) => {
// Обновляем реакции в текущем открытом чате // Обновляем реакции в текущем открытом чате
if Some(update.chat_id) == self.current_chat_id { if Some(update.chat_id) == self.current_chat_id {
if let Some(msg) = self.current_chat_messages.iter_mut().find(|m| m.id == update.message_id) { if let Some(msg) = self
.current_chat_messages
.iter_mut()
.find(|m| m.id == update.message_id)
{
// Извлекаем реакции из interaction_info // Извлекаем реакции из interaction_info
msg.reactions = update msg.reactions = update
.interaction_info .interaction_info
@@ -656,8 +686,12 @@ impl TdClient {
.iter() .iter()
.filter_map(|reaction| { .filter_map(|reaction| {
let emoji = match &reaction.r#type { let emoji = match &reaction.r#type {
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(), tdlib_rs::enums::ReactionType::Emoji(e) => {
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, e.emoji.clone()
}
tdlib_rs::enums::ReactionType::CustomEmoji(_) => {
return None
}
}; };
Some(ReactionInfo { Some(ReactionInfo {
@@ -697,9 +731,10 @@ impl TdClient {
} }
// Ищем позицию в Main списке (если есть) // Ищем позицию в Main списке (если есть)
let main_position = td_chat.positions.iter().find(|pos| { let main_position = td_chat
matches!(pos.list, ChatList::Main) .positions
}); .iter()
.find(|pos| matches!(pos.list, ChatList::Main));
// Получаем order и is_pinned из позиции, или используем значения по умолчанию // Получаем order и is_pinned из позиции, или используем значения по умолчанию
let (order, is_pinned) = main_position let (order, is_pinned) = main_position
@@ -716,7 +751,9 @@ impl TdClient {
let username = match &td_chat.r#type { let username = match &td_chat.r#type {
ChatType::Private(private) => { ChatType::Private(private) => {
// Ограничиваем размер chat_user_ids // Ограничиваем размер chat_user_ids
if self.chat_user_ids.len() >= MAX_CHAT_USER_IDS && !self.chat_user_ids.contains_key(&td_chat.id) { if self.chat_user_ids.len() >= MAX_CHAT_USER_IDS
&& !self.chat_user_ids.contains_key(&td_chat.id)
{
// Удаляем случайную запись (первую найденную) // Удаляем случайную запись (первую найденную)
if let Some(&key) = self.chat_user_ids.keys().next() { if let Some(&key) = self.chat_user_ids.keys().next() {
self.chat_user_ids.remove(&key); self.chat_user_ids.remove(&key);
@@ -724,7 +761,9 @@ impl TdClient {
} }
self.chat_user_ids.insert(td_chat.id, private.user_id); self.chat_user_ids.insert(td_chat.id, private.user_id);
// Проверяем, есть ли уже username в кэше (peek не обновляет LRU) // Проверяем, есть ли уже username в кэше (peek не обновляет LRU)
self.user_usernames.peek(&private.user_id).map(|u| format!("@{}", u)) self.user_usernames
.peek(&private.user_id)
.map(|u| format!("@{}", u))
} }
_ => None, _ => None,
}; };
@@ -784,7 +823,13 @@ impl TdClient {
// Ограничиваем количество чатов // Ограничиваем количество чатов
if self.chats.len() > MAX_CHATS { if self.chats.len() > MAX_CHATS {
// Удаляем чат с наименьшим order (наименее активный) // Удаляем чат с наименьшим order (наименее активный)
if let Some(min_idx) = self.chats.iter().enumerate().min_by_key(|(_, c)| c.order).map(|(i, _)| i) { if let Some(min_idx) = self
.chats
.iter()
.enumerate()
.min_by_key(|(_, c)| c.order)
.map(|(i, _)| i)
{
self.chats.remove(min_idx); self.chats.remove(min_idx);
} }
} }
@@ -891,11 +936,7 @@ impl TdClient {
.unwrap_or_default() .unwrap_or_default()
}; };
Some(ReplyInfo { Some(ReplyInfo { message_id: reply.message_id, sender_name, text })
message_id: reply.message_id,
sender_name,
text,
})
} }
_ => None, _ => None,
} }
@@ -905,10 +946,7 @@ impl TdClient {
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> { fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
message.forward_info.as_ref().map(|info| { message.forward_info.as_ref().map(|info| {
let sender_name = self.get_origin_sender_name(&info.origin); let sender_name = self.get_origin_sender_name(&info.origin);
ForwardInfo { ForwardInfo { sender_name, date: info.date }
sender_name,
date: info.date,
}
}) })
} }
@@ -944,24 +982,24 @@ impl TdClient {
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String { fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
use tdlib_rs::enums::MessageOrigin; use tdlib_rs::enums::MessageOrigin;
match origin { match origin {
MessageOrigin::User(u) => { MessageOrigin::User(u) => self
self.user_names.peek(&u.sender_user_id) .user_names
.cloned() .peek(&u.sender_user_id)
.unwrap_or_else(|| format!("User_{}", u.sender_user_id)) .cloned()
} .unwrap_or_else(|| format!("User_{}", u.sender_user_id)),
MessageOrigin::Chat(c) => { MessageOrigin::Chat(c) => self
self.chats.iter() .chats
.find(|chat| chat.id == c.sender_chat_id) .iter()
.map(|chat| chat.title.clone()) .find(|chat| chat.id == c.sender_chat_id)
.unwrap_or_else(|| "Чат".to_string()) .map(|chat| chat.title.clone())
} .unwrap_or_else(|| "Чат".to_string()),
MessageOrigin::HiddenUser(h) => h.sender_name.clone(), MessageOrigin::HiddenUser(h) => h.sender_name.clone(),
MessageOrigin::Channel(c) => { MessageOrigin::Channel(c) => self
self.chats.iter() .chats
.find(|chat| chat.id == c.chat_id) .iter()
.map(|chat| chat.title.clone()) .find(|chat| chat.id == c.chat_id)
.unwrap_or_else(|| "Канал".to_string()) .map(|chat| chat.title.clone())
} .unwrap_or_else(|| "Канал".to_string()),
} }
} }
@@ -1032,19 +1070,17 @@ impl TdClient {
functions::get_message(chat_id, msg_id, self.client_id).await functions::get_message(chat_id, msg_id, self.client_id).await
{ {
let sender_name = match &msg.sender_id { let sender_name = match &msg.sender_id {
tdlib_rs::enums::MessageSender::User(user) => { tdlib_rs::enums::MessageSender::User(user) => self
self.user_names .user_names
.get(&user.user_id) .get(&user.user_id)
.cloned() .cloned()
.unwrap_or_else(|| format!("User_{}", user.user_id)) .unwrap_or_else(|| format!("User_{}", user.user_id)),
} tdlib_rs::enums::MessageSender::Chat(chat) => self
tdlib_rs::enums::MessageSender::Chat(chat) => { .chats
self.chats .iter()
.iter() .find(|c| c.id == chat.chat_id)
.find(|c| c.id == chat.chat_id) .map(|c| c.title.clone())
.map(|c| c.title.clone()) .unwrap_or_else(|| "Чат".to_string()),
.unwrap_or_else(|| "Чат".to_string())
}
}; };
let (content, _) = extract_message_text_static(&msg); let (content, _) = extract_message_text_static(&msg);
reply_cache.insert(msg_id, (sender_name, content)); reply_cache.insert(msg_id, (sender_name, content));
@@ -1068,12 +1104,7 @@ impl TdClient {
/// Отправка номера телефона /// Отправка номера телефона
pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> { pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> {
let result = functions::set_authentication_phone_number( let result = functions::set_authentication_phone_number(phone, None, self.client_id).await;
phone,
None,
self.client_id,
)
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1103,12 +1134,7 @@ impl TdClient {
/// Загрузка списка чатов /// Загрузка списка чатов
pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
let result = functions::load_chats( let result = functions::load_chats(Some(ChatList::Main), limit, self.client_id).await;
Some(ChatList::Main),
limit,
self.client_id,
)
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1118,16 +1144,10 @@ impl TdClient {
/// Загрузка чатов для конкретной папки /// Загрузка чатов для конкретной папки
pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> { pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
let chat_list = ChatList::Folder(tdlib_rs::types::ChatListFolder { let chat_list =
chat_folder_id: folder_id, ChatList::Folder(tdlib_rs::types::ChatListFolder { chat_folder_id: folder_id });
});
let result = functions::load_chats( let result = functions::load_chats(Some(chat_list), limit, self.client_id).await;
Some(chat_list),
limit,
self.client_id,
)
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1155,9 +1175,9 @@ impl TdClient {
let result = functions::get_chat_history( let result = functions::get_chat_history(
chat_id, chat_id,
from_message_id, from_message_id,
0, // offset 0, // offset
limit, limit,
false, // only_local - загружаем с сервера! false, // only_local - загружаем с сервера!
self.client_id, self.client_id,
) )
.await; .await;
@@ -1209,8 +1229,8 @@ impl TdClient {
let _ = functions::view_messages( let _ = functions::view_messages(
chat_id, chat_id,
message_ids, message_ids,
None, // source None, // source
true, // force_read true, // force_read
self.client_id, self.client_id,
) )
.await; .await;
@@ -1223,14 +1243,14 @@ impl TdClient {
pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> { pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> {
let result = functions::search_chat_messages( let result = functions::search_chat_messages(
chat_id, chat_id,
"".to_string(), // query "".to_string(), // query
None, // sender_id None, // sender_id
0, // from_message_id 0, // from_message_id
0, // offset 0, // offset
100, // limit 100, // limit
Some(SearchMessagesFilter::Pinned), // filter Some(SearchMessagesFilter::Pinned), // filter
0, // message_thread_id 0, // message_thread_id
0, // saved_messages_topic_id 0, // saved_messages_topic_id
self.client_id, self.client_id,
) )
.await; .await;
@@ -1279,7 +1299,11 @@ impl TdClient {
} }
/// Поиск сообщений в чате по тексту /// Поиск сообщений в чате по тексту
pub async fn search_messages(&mut self, chat_id: i64, query: &str) -> Result<Vec<MessageInfo>, String> { pub async fn search_messages(
&mut self,
chat_id: i64,
query: &str,
) -> Result<Vec<MessageInfo>, String> {
if query.trim().is_empty() { if query.trim().is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
@@ -1287,13 +1311,13 @@ impl TdClient {
let result = functions::search_chat_messages( let result = functions::search_chat_messages(
chat_id, chat_id,
query.to_string(), query.to_string(),
None, // sender_id None, // sender_id
0, // from_message_id 0, // from_message_id
0, // offset 0, // offset
50, // limit 50, // limit
None, // filter (no filter = search by text) None, // filter (no filter = search by text)
0, // message_thread_id 0, // message_thread_id
0, // saved_messages_topic_id 0, // saved_messages_topic_id
self.client_id, self.client_id,
) )
.await; .await;
@@ -1359,8 +1383,12 @@ impl TdClient {
profile.online_status = Some(match user.status { profile.online_status = Some(match user.status {
tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(), tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(),
tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(), tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(),
tdlib_rs::enums::UserStatus::LastWeek(_) => "Был(а) на этой неделе".to_string(), tdlib_rs::enums::UserStatus::LastWeek(_) => {
tdlib_rs::enums::UserStatus::LastMonth(_) => "Был(а) в этом месяце".to_string(), "Был(а) на этой неделе".to_string()
}
tdlib_rs::enums::UserStatus::LastMonth(_) => {
"Был(а) в этом месяце".to_string()
}
tdlib_rs::enums::UserStatus::Offline(offline) => { tdlib_rs::enums::UserStatus::Offline(offline) => {
crate::utils::format_was_online(offline.was_online) crate::utils::format_was_online(offline.was_online)
} }
@@ -1369,8 +1397,10 @@ impl TdClient {
} }
// Bio (getUserFullInfo) // Bio (getUserFullInfo)
let full_info_result = functions::get_user_full_info(private_chat.user_id, self.client_id).await; let full_info_result =
if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result { functions::get_user_full_info(private_chat.user_id, self.client_id).await;
if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result
{
if let Some(bio_obj) = full_info.bio { if let Some(bio_obj) = full_info.bio {
profile.bio = Some(bio_obj.text); profile.bio = Some(bio_obj.text);
} }
@@ -1381,14 +1411,21 @@ impl TdClient {
profile.is_group = true; profile.is_group = true;
// Получаем информацию о группе // Получаем информацию о группе
let group_result = functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; let group_result =
functions::get_basic_group(basic_group.basic_group_id, self.client_id).await;
if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result { if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result {
profile.member_count = Some(group.member_count); profile.member_count = Some(group.member_count);
} }
// Полная информация о группе // Полная информация о группе
let full_info_result = functions::get_basic_group_full_info(basic_group.basic_group_id, self.client_id).await; let full_info_result = functions::get_basic_group_full_info(
if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = full_info_result { basic_group.basic_group_id,
self.client_id,
)
.await;
if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) =
full_info_result
{
if !full_info.description.is_empty() { if !full_info.description.is_empty() {
profile.description = Some(full_info.description); profile.description = Some(full_info.description);
} }
@@ -1399,9 +1436,14 @@ impl TdClient {
} }
ChatType::Supergroup(supergroup) => { ChatType::Supergroup(supergroup) => {
// Получаем информацию о супергруппе // Получаем информацию о супергруппе
let sg_result = functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; let sg_result =
functions::get_supergroup(supergroup.supergroup_id, self.client_id).await;
if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result { if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result {
profile.chat_type = if sg.is_channel { "Канал".to_string() } else { "Супергруппа".to_string() }; profile.chat_type = if sg.is_channel {
"Канал".to_string()
} else {
"Супергруппа".to_string()
};
profile.is_group = !sg.is_channel; profile.is_group = !sg.is_channel;
profile.member_count = Some(sg.member_count); profile.member_count = Some(sg.member_count);
@@ -1414,8 +1456,12 @@ impl TdClient {
} }
// Полная информация о супергруппе // Полная информация о супергруппе
let full_info_result = functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id).await; let full_info_result =
if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = full_info_result { functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id)
.await;
if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) =
full_info_result
{
if !full_info.description.is_empty() { if !full_info.description.is_empty() {
profile.description = Some(full_info.description); profile.description = Some(full_info.description);
} }
@@ -1451,9 +1497,9 @@ impl TdClient {
let result = functions::get_chat_history( let result = functions::get_chat_history(
chat_id, chat_id,
from_message_id, from_message_id,
0, // offset 0, // offset
limit, limit,
false, // only_local false, // only_local
self.client_id, self.client_id,
) )
.await; .await;
@@ -1497,11 +1543,9 @@ impl TdClient {
/// Получение моего user_id /// Получение моего user_id
pub async fn get_me(&self) -> Result<i64, String> { pub async fn get_me(&self) -> Result<i64, String> {
match functions::get_me(self.client_id).await { match functions::get_me(self.client_id).await {
Ok(user) => { Ok(user) => match user {
match user { User::User(u) => Ok(u.id),
User::User(u) => Ok(u.id), },
}
}
Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)), Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)),
} }
} }
@@ -1513,30 +1557,37 @@ impl TdClient {
0, // message_thread_id 0, // message_thread_id
Some(action), Some(action),
self.client_id, self.client_id,
).await; )
.await;
} }
/// Отправка текстового сообщения с поддержкой Markdown и reply /// Отправка текстового сообщения с поддержкой Markdown и reply
pub async fn send_message(&self, chat_id: i64, text: String, reply_to_message_id: Option<i64>, reply_info: Option<ReplyInfo>) -> Result<MessageInfo, String> { pub async fn send_message(
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage}; &self,
use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo}; chat_id: i64,
text: String,
reply_to_message_id: Option<i64>,
reply_info: Option<ReplyInfo>,
) -> Result<MessageInfo, String> {
use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, TextParseMode};
use tdlib_rs::types::{
FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown,
};
// Парсим markdown в тексте // Парсим markdown в тексте
let formatted_text = match functions::parse_text_entities( let formatted_text = match functions::parse_text_entities(
text.clone(), text.clone(),
TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }),
self.client_id, self.client_id,
).await { )
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText { .await
text: ft.text, {
entities: ft.entities, Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => {
}, FormattedText { text: ft.text, entities: ft.entities }
}
Err(_) => { Err(_) => {
// Если парсинг не удался, отправляем как plain text // Если парсинг не удался, отправляем как plain text
FormattedText { FormattedText { text: text.clone(), entities: vec![] }
text: text.clone(),
entities: vec![],
}
} }
}; };
@@ -1558,9 +1609,9 @@ impl TdClient {
let result = functions::send_message( let result = functions::send_message(
chat_id, chat_id,
0, // message_thread_id 0, // message_thread_id
reply_to, reply_to,
None, // options None, // options
content, content,
self.client_id, self.client_id,
) )
@@ -1592,7 +1643,6 @@ impl TdClient {
} }
} }
/// Получить доступные реакции для сообщения /// Получить доступные реакции для сообщения
pub async fn get_message_available_reactions( pub async fn get_message_available_reactions(
&mut self, &mut self,
@@ -1653,9 +1703,9 @@ impl TdClient {
message_id: i64, message_id: i64,
emoji: String, emoji: String,
) -> Result<(), String> { ) -> Result<(), String> {
use tdlib_rs::enums::ReactionType;
use tdlib_rs::functions; use tdlib_rs::functions;
use tdlib_rs::types::ReactionTypeEmoji; use tdlib_rs::types::ReactionTypeEmoji;
use tdlib_rs::enums::ReactionType;
let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji }); let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji });
@@ -1678,17 +1728,18 @@ impl TdClient {
/// Редактирование текстового сообщения с поддержкой Markdown /// Редактирование текстового сообщения с поддержкой Markdown
/// Устанавливает черновик для чата через TDLib API /// Устанавливает черновик для чата через TDLib API
pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
use tdlib_rs::types::{FormattedText, InputMessageText, DraftMessage};
use tdlib_rs::enums::InputMessageContent; use tdlib_rs::enums::InputMessageContent;
use tdlib_rs::types::{DraftMessage, FormattedText, InputMessageText};
if text.is_empty() { if text.is_empty() {
// Очищаем черновик // Очищаем черновик
let result = functions::set_chat_draft_message( let result = functions::set_chat_draft_message(
chat_id, chat_id,
0, // message_thread_id 0, // message_thread_id
None, // draft_message (None = очистить) None, // draft_message (None = очистить)
self.client_id, self.client_id,
).await; )
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1696,10 +1747,7 @@ impl TdClient {
} }
} else { } else {
// Создаём черновик // Создаём черновик
let formatted_text = FormattedText { let formatted_text = FormattedText { text: text.clone(), entities: vec![] };
text: text.clone(),
entities: vec![],
};
let input_message = InputMessageContent::InputMessageText(InputMessageText { let input_message = InputMessageContent::InputMessageText(InputMessageText {
text: formatted_text, text: formatted_text,
@@ -1718,7 +1766,8 @@ impl TdClient {
0, // message_thread_id 0, // message_thread_id
Some(draft), Some(draft),
self.client_id, self.client_id,
).await; )
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1727,26 +1776,29 @@ impl TdClient {
} }
} }
pub async fn edit_message(&self, chat_id: i64, message_id: i64, text: String) -> Result<MessageInfo, String> { pub async fn edit_message(
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; &self,
chat_id: i64,
message_id: i64,
text: String,
) -> Result<MessageInfo, String> {
use tdlib_rs::enums::{InputMessageContent, TextParseMode}; use tdlib_rs::enums::{InputMessageContent, TextParseMode};
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown};
// Парсим markdown в тексте // Парсим markdown в тексте
let formatted_text = match functions::parse_text_entities( let formatted_text = match functions::parse_text_entities(
text.clone(), text.clone(),
TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }),
self.client_id, self.client_id,
).await { )
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText { .await
text: ft.text, {
entities: ft.entities, Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => {
}, FormattedText { text: ft.text, entities: ft.entities }
}
Err(_) => { Err(_) => {
// Если парсинг не удался, отправляем как plain text // Если парсинг не удался, отправляем как plain text
FormattedText { FormattedText { text: text.clone(), entities: vec![] }
text: text.clone(),
entities: vec![],
}
} }
}; };
@@ -1756,13 +1808,8 @@ impl TdClient {
clear_draft: true, clear_draft: true,
}); });
let result = functions::edit_message_text( let result =
chat_id, functions::edit_message_text(chat_id, message_id, content, self.client_id).await;
message_id,
content,
self.client_id,
)
.await;
match result { match result {
Ok(tdlib_rs::enums::Message::Message(msg)) => { Ok(tdlib_rs::enums::Message::Message(msg)) => {
@@ -1790,14 +1837,13 @@ impl TdClient {
/// Удаление сообщений /// Удаление сообщений
/// revoke = true удаляет для всех, false только для себя /// revoke = true удаляет для всех, false только для себя
pub async fn delete_messages(&self, chat_id: i64, message_ids: Vec<i64>, revoke: bool) -> Result<(), String> { pub async fn delete_messages(
let result = functions::delete_messages( &self,
chat_id, chat_id: i64,
message_ids, message_ids: Vec<i64>,
revoke, revoke: bool,
self.client_id, ) -> Result<(), String> {
) let result = functions::delete_messages(chat_id, message_ids, revoke, self.client_id).await;
.await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -1806,13 +1852,18 @@ impl TdClient {
} }
/// Пересылка сообщений /// Пересылка сообщений
pub async fn forward_messages(&self, to_chat_id: i64, from_chat_id: i64, message_ids: Vec<i64>) -> Result<(), String> { pub async fn forward_messages(
&self,
to_chat_id: i64,
from_chat_id: i64,
message_ids: Vec<i64>,
) -> Result<(), String> {
let result = functions::forward_messages( let result = functions::forward_messages(
to_chat_id, to_chat_id,
0, // message_thread_id 0, // message_thread_id
from_chat_id, from_chat_id,
message_ids, message_ids,
None, // options None, // options
false, // send_copy false, // send_copy
false, // remove_caption false, // remove_caption
self.client_id, self.client_id,
@@ -1832,8 +1883,8 @@ impl TdClient {
let _ = functions::view_messages( let _ = functions::view_messages(
chat_id, chat_id,
message_ids, message_ids,
None, // source None, // source
true, // force_read true, // force_read
self.client_id, self.client_id,
) )
.await; .await;
@@ -1847,7 +1898,8 @@ impl TdClient {
const BATCH_SIZE: usize = 5; const BATCH_SIZE: usize = 5;
// Убираем дубликаты и уже загруженные // Убираем дубликаты и уже загруженные
self.pending_user_ids.retain(|id| !self.user_names.contains_key(id)); self.pending_user_ids
.retain(|id| !self.user_names.contains_key(id));
self.pending_user_ids.dedup(); self.pending_user_ids.dedup();
// Берём последние BATCH_SIZE элементов // Берём последние BATCH_SIZE элементов
@@ -1885,16 +1937,17 @@ impl TdClient {
/// Статическая функция для извлечения текста и entities сообщения (без &self) /// Статическая функция для извлечения текста и entities сообщения (без &self)
fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>) { fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>) {
match &message.content { match &message.content {
MessageContent::MessageText(text) => { MessageContent::MessageText(text) => (text.text.text.clone(), text.text.entities.clone()),
(text.text.text.clone(), text.text.entities.clone())
}
MessageContent::MessagePhoto(photo) => { MessageContent::MessagePhoto(photo) => {
if photo.caption.text.is_empty() { if photo.caption.text.is_empty() {
("[Фото]".to_string(), vec![]) ("[Фото]".to_string(), vec![])
} else { } else {
// Добавляем смещение для "[Фото] " к entities // Добавляем смещение для "[Фото] " к entities
let prefix_len = "[Фото] ".chars().count() as i32; let prefix_len = "[Фото] ".chars().count() as i32;
let adjusted_entities: Vec<TextEntity> = photo.caption.entities.iter() let adjusted_entities: Vec<TextEntity> = photo
.caption
.entities
.iter()
.map(|e| TextEntity { .map(|e| TextEntity {
offset: e.offset + prefix_len, offset: e.offset + prefix_len,
length: e.length, length: e.length,
@@ -1909,7 +1962,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
("[Видео]".to_string(), vec![]) ("[Видео]".to_string(), vec![])
} else { } else {
let prefix_len = "[Видео] ".chars().count() as i32; let prefix_len = "[Видео] ".chars().count() as i32;
let adjusted_entities: Vec<TextEntity> = video.caption.entities.iter() let adjusted_entities: Vec<TextEntity> = video
.caption
.entities
.iter()
.map(|e| TextEntity { .map(|e| TextEntity {
offset: e.offset + prefix_len, offset: e.offset + prefix_len,
length: e.length, length: e.length,
@@ -1932,7 +1988,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
("[GIF]".to_string(), vec![]) ("[GIF]".to_string(), vec![])
} else { } else {
let prefix_len = "[GIF] ".chars().count() as i32; let prefix_len = "[GIF] ".chars().count() as i32;
let adjusted_entities: Vec<TextEntity> = anim.caption.entities.iter() let adjusted_entities: Vec<TextEntity> = anim
.caption
.entities
.iter()
.map(|e| TextEntity { .map(|e| TextEntity {
offset: e.offset + prefix_len, offset: e.offset + prefix_len,
length: e.length, length: e.length,
@@ -1942,9 +2001,7 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
(format!("[GIF] {}", anim.caption.text), adjusted_entities) (format!("[GIF] {}", anim.caption.text), adjusted_entities)
} }
} }
MessageContent::MessageAudio(audio) => { MessageContent::MessageAudio(audio) => (format!("[Аудио: {}]", audio.audio.title), vec![]),
(format!("[Аудио: {}]", audio.audio.title), vec![])
}
MessageContent::MessageCall(_) => ("[Звонок]".to_string(), vec![]), MessageContent::MessageCall(_) => ("[Звонок]".to_string(), vec![]),
MessageContent::MessagePoll(poll) => { MessageContent::MessagePoll(poll) => {
(format!("[Опрос: {}]", poll.poll.question.text), vec![]) (format!("[Опрос: {}]", poll.poll.question.text), vec![])

View File

@@ -1,13 +1,13 @@
pub mod client; pub mod client;
pub use client::TdClient; pub use client::ChatInfo;
pub use client::UserOnlineStatus; pub use client::FolderInfo;
pub use client::ForwardInfo;
pub use client::MessageInfo;
pub use client::NetworkState; pub use client::NetworkState;
pub use client::ProfileInfo; pub use client::ProfileInfo;
pub use client::ChatInfo;
pub use client::MessageInfo;
pub use client::ReactionInfo; pub use client::ReactionInfo;
pub use client::ReplyInfo; pub use client::ReplyInfo;
pub use client::ForwardInfo; pub use client::TdClient;
pub use client::FolderInfo; pub use client::UserOnlineStatus;
pub use tdlib_rs::enums::ChatAction; pub use tdlib_rs::enums::ChatAction;

View File

@@ -1,3 +1,5 @@
use crate::app::App;
use crate::tdlib::client::AuthState;
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
@@ -5,8 +7,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
use crate::tdlib::client::AuthState;
pub fn render(f: &mut Frame, app: &App) { pub fn render(f: &mut Frame, app: &App) {
let area = f.area(); let area = f.area();

View File

@@ -1,11 +1,11 @@
use crate::app::App;
use crate::tdlib::UserOnlineStatus;
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, Paragraph}, widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
use crate::tdlib::UserOnlineStatus;
pub fn render(f: &mut Frame, area: Rect, app: &mut App) { pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
let chat_chunks = Layout::default() let chat_chunks = Layout::default()
@@ -54,7 +54,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
let prefix = if is_selected { "" } else { " " }; let prefix = if is_selected { "" } else { " " };
let username_text = chat.username.as_ref() let username_text = chat
.username
.as_ref()
.map(|u| format!(" {}", u)) .map(|u| format!(" {}", u))
.unwrap_or_default(); .unwrap_or_default();
@@ -78,7 +80,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
String::new() String::new()
}; };
let content = format!("{}{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, draft_badge, unread_badge); let content = format!(
"{}{}{}{}{}{}{}{}{}",
prefix,
status_icon,
pin_icon,
mute_icon,
chat.title,
username_text,
mention_badge,
draft_badge,
unread_badge
);
// Цвет: онлайн — зелёные, остальные — белые // Цвет: онлайн — зелёные, остальные — белые
let style = match app.td_client.get_user_status_by_chat_id(chat.id) { let style = match app.td_client.get_user_status_by_chat_id(chat.id) {
@@ -100,13 +113,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
Block::default().borders(Borders::ALL) Block::default().borders(Borders::ALL)
}; };
let chats_list = List::new(items) let chats_list = List::new(items).block(block).highlight_style(
.block(block) Style::default()
.highlight_style( .add_modifier(Modifier::ITALIC)
Style::default() .fg(Color::Yellow),
.add_modifier(Modifier::ITALIC) );
.fg(Color::Yellow),
);
f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state); f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state);
@@ -119,8 +130,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
let formatted = format_was_online(*was_online); let formatted = format_was_online(*was_online);
(formatted, Color::Gray) (formatted, Color::Gray)
} }
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray), Some(UserOnlineStatus::LastWeek) => {
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray), ("был(а) на этой неделе".to_string(), Color::DarkGray)
}
Some(UserOnlineStatus::LastMonth) => {
("был(а) в этом месяце".to_string(), Color::DarkGray)
}
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray), Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray),
None => ("".to_string(), Color::DarkGray), // Для групп/каналов None => ("".to_string(), Color::DarkGray), // Для групп/каналов
} }
@@ -131,14 +146,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
if let Some(chat) = filtered.get(i) { if let Some(chat) = filtered.get(i) {
match app.td_client.get_user_status_by_chat_id(chat.id) { match app.td_client.get_user_status_by_chat_id(chat.id) {
Some(UserOnlineStatus::Online) => ("● онлайн".to_string(), Color::Green), Some(UserOnlineStatus::Online) => ("● онлайн".to_string(), Color::Green),
Some(UserOnlineStatus::Recently) => ("был(а) недавно".to_string(), Color::Yellow), Some(UserOnlineStatus::Recently) => {
("был(а) недавно".to_string(), Color::Yellow)
}
Some(UserOnlineStatus::Offline(was_online)) => { Some(UserOnlineStatus::Offline(was_online)) => {
let formatted = format_was_online(*was_online); let formatted = format_was_online(*was_online);
(formatted, Color::Gray) (formatted, Color::Gray)
} }
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray), Some(UserOnlineStatus::LastWeek) => {
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray), ("был(а) на этой неделе".to_string(), Color::DarkGray)
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray), }
Some(UserOnlineStatus::LastMonth) => {
("был(а) в этом месяце".to_string(), Color::DarkGray)
}
Some(UserOnlineStatus::LongTimeAgo) => {
("был(а) давно".to_string(), Color::DarkGray)
}
None => ("".to_string(), Color::DarkGray), None => ("".to_string(), Color::DarkGray),
} }
} else { } else {

View File

@@ -1,11 +1,11 @@
use crate::app::App;
use crate::tdlib::NetworkState;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
widgets::Paragraph, widgets::Paragraph,
Frame, Frame,
}; };
use crate::app::App;
use crate::tdlib::NetworkState;
pub fn render(f: &mut Frame, area: Rect, app: &App) { pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Индикатор состояния сети // Индикатор состояния сети
@@ -26,7 +26,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} else if app.selected_chat_id.is_some() { } else if app.selected_chat_id.is_some() {
format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator) format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
} else { } else {
format!(" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator) format!(
" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ",
network_indicator
)
}; };
let style = if matches!(app.td_client.network_state, NetworkState::WaitingForNetwork) { let style = if matches!(app.td_client.network_state, NetworkState::WaitingForNetwork) {

View File

@@ -1,10 +1,10 @@
use crate::app::App;
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
pub fn render(f: &mut Frame, app: &App) { pub fn render(f: &mut Frame, app: &App) {
let area = f.area(); let area = f.area();
@@ -18,10 +18,7 @@ pub fn render(f: &mut Frame, app: &App) {
]) ])
.split(area); .split(area);
let message = app let message = app.status_message.as_deref().unwrap_or("Загрузка...");
.status_message
.as_deref()
.unwrap_or("Загрузка...");
let loading = Paragraph::new(message) let loading = Paragraph::new(message)
.style( .style(
@@ -30,11 +27,7 @@ pub fn render(f: &mut Frame, app: &App) {
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
) )
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(Block::default().borders(Borders::ALL).title(" TTUI "));
Block::default()
.borders(Borders::ALL)
.title(" TTUI "),
);
f.render_widget(loading, chunks[1]); f.render_widget(loading, chunks[1]);
} }

View File

@@ -1,3 +1,5 @@
use super::{chat_list, footer, messages};
use crate::app::App;
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
@@ -5,8 +7,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
use super::{chat_list, messages, footer};
/// Порог ширины для компактного режима (одна панель) /// Порог ширины для компактного режима (одна панель)
const COMPACT_WIDTH: u16 = 80; const COMPACT_WIDTH: u16 = 80;
@@ -81,11 +81,8 @@ fn render_folders(f: &mut Frame, area: Rect, app: &App) {
} }
let folders_line = Line::from(spans); let folders_line = Line::from(spans);
let folders_widget = Paragraph::new(folders_line).block( let folders_widget =
Block::default() Paragraph::new(folders_line).block(Block::default().title(" TTUI ").borders(Borders::ALL));
.title(" TTUI ")
.borders(Borders::ALL),
);
f.render_widget(folders_widget, area); f.render_widget(folders_widget, area);
} }

View File

@@ -1,3 +1,5 @@
use crate::app::App;
use crate::utils::{format_date, format_timestamp_with_tz, get_day};
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
@@ -5,8 +7,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
use crate::utils::{format_timestamp_with_tz, format_date, get_day};
use tdlib_rs::enums::TextEntityType; use tdlib_rs::enums::TextEntityType;
use tdlib_rs::types::TextEntity; use tdlib_rs::types::TextEntity;
@@ -58,9 +58,16 @@ impl CharStyle {
} }
/// Преобразует текст с entities в вектор стилизованных Span (owned) /// Преобразует текст с entities в вектор стилизованных Span (owned)
fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Color) -> Vec<Span<'static>> { fn format_text_with_entities(
text: &str,
entities: &[TextEntity],
base_color: Color,
) -> Vec<Span<'static>> {
if entities.is_empty() { if entities.is_empty() {
return vec![Span::styled(text.to_string(), Style::default().fg(base_color))]; return vec![Span::styled(
text.to_string(),
Style::default().fg(base_color),
)];
} }
// Создаём массив стилей для каждого символа // Создаём массив стилей для каждого символа
@@ -82,9 +89,13 @@ fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Co
char_styles[i].code = true char_styles[i].code = true
} }
TextEntityType::Spoiler => char_styles[i].spoiler = true, TextEntityType::Spoiler => char_styles[i].spoiler = true,
TextEntityType::Url | TextEntityType::TextUrl(_) | TextEntityType::EmailAddress TextEntityType::Url
| TextEntityType::TextUrl(_)
| TextEntityType::EmailAddress
| TextEntityType::PhoneNumber => char_styles[i].url = true, | TextEntityType::PhoneNumber => char_styles[i].url = true,
TextEntityType::Mention | TextEntityType::MentionName(_) => char_styles[i].mention = true, TextEntityType::Mention | TextEntityType::MentionName(_) => {
char_styles[i].mention = true
}
_ => {} _ => {}
} }
} }
@@ -144,7 +155,12 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool {
} }
/// Рендерит текст инпута с блочным курсором /// Рендерит текст инпута с блочным курсором
fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: Color) -> Line<'static> { fn render_input_with_cursor(
prefix: &str,
text: &str,
cursor_pos: usize,
color: Color,
) -> Line<'static> {
let chars: Vec<char> = text.chars().collect(); let chars: Vec<char> = text.chars().collect();
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())]; let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
@@ -186,10 +202,7 @@ struct WrappedLine {
/// Возвращает строки с информацией о позициях для корректного применения entities /// Возвращает строки с информацией о позициях для корректного применения entities
fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> { fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
if max_width == 0 { if max_width == 0 {
return vec![WrappedLine { return vec![WrappedLine { text: text.to_string(), start_offset: 0 }];
text: text.to_string(),
start_offset: 0,
}];
} }
let mut result = Vec::new(); let mut result = Vec::new();
@@ -263,10 +276,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
} }
if result.is_empty() { if result.is_empty() {
result.push(WrappedLine { result.push(WrappedLine { text: String::new(), start_offset: 0 });
text: String::new(),
start_offset: 0,
});
} }
result result
@@ -368,24 +378,28 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}; };
// Chat header с typing status // Chat header с typing status
let typing_action = app.td_client.typing_status.as_ref().map(|(_, action, _)| action.clone()); let typing_action = app
.td_client
.typing_status
.as_ref()
.map(|(_, action, _)| action.clone());
let header_line = if let Some(action) = typing_action { let header_line = if let Some(action) = typing_action {
// Показываем typing status: "👤 Имя @username печатает..." // Показываем typing status: "👤 Имя @username печатает..."
let mut spans = vec![ let mut spans = vec![Span::styled(
Span::styled( format!("👤 {}", chat.title),
format!("👤 {}", chat.title), Style::default()
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), .fg(Color::Cyan)
), .add_modifier(Modifier::BOLD),
]; )];
if let Some(username) = &chat.username { if let Some(username) = &chat.username {
spans.push(Span::styled( spans
format!(" {}", username), .push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray)));
Style::default().fg(Color::Gray),
));
} }
spans.push(Span::styled( spans.push(Span::styled(
format!(" {}", action), format!(" {}", action),
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC), Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC),
)); ));
Line::from(spans) Line::from(spans)
} else { } else {
@@ -396,17 +410,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}; };
Line::from(Span::styled( Line::from(Span::styled(
header_text, header_text,
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)) ))
}; };
let header = Paragraph::new(header_line) let header = Paragraph::new(header_line).block(Block::default().borders(Borders::ALL));
.block(Block::default().borders(Borders::ALL));
f.render_widget(header, message_chunks[0]); f.render_widget(header, message_chunks[0]);
// Pinned bar (если есть закреплённое сообщение) // Pinned bar (если есть закреплённое сообщение)
if let Some(pinned_msg) = &app.td_client.current_pinned_message { if let Some(pinned_msg) = &app.td_client.current_pinned_message {
let pinned_preview: String = pinned_msg.content.chars().take(40).collect(); let pinned_preview: String = pinned_msg.content.chars().take(40).collect();
let ellipsis = if pinned_msg.content.chars().count() > 40 { "..." } else { "" }; let ellipsis = if pinned_msg.content.chars().count() > 40 {
"..."
} else {
""
};
let pinned_datetime = crate::utils::format_datetime(pinned_msg.date); let pinned_datetime = crate::utils::format_datetime(pinned_msg.date);
let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis); let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis);
let pinned_hint = "Ctrl+P"; let pinned_hint = "Ctrl+P";
@@ -421,8 +440,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
Span::raw(" ".repeat(padding)), Span::raw(" ".repeat(padding)),
Span::styled(pinned_hint, Style::default().fg(Color::Gray)), Span::styled(pinned_hint, Style::default().fg(Color::Gray)),
]); ]);
let pinned_bar = Paragraph::new(pinned_line) let pinned_bar =
.style(Style::default().bg(Color::Rgb(40, 20, 40))); Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
f.render_widget(pinned_bar, message_chunks[1]); f.render_widget(pinned_bar, message_chunks[1]);
} }
@@ -484,9 +503,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} }
let sender_style = if msg.is_outgoing { let sender_style = if msg.is_outgoing {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD)
} else { } else {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
}; };
if msg.is_outgoing { if msg.is_outgoing {
@@ -540,16 +563,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
])); ]));
} else { } else {
// Forward слева для входящих // Forward слева для входящих
lines.push(Line::from(vec![ lines.push(Line::from(vec![Span::styled(
Span::styled(forward_line, Style::default().fg(Color::Magenta)), forward_line,
])); Style::default().fg(Color::Magenta),
)]));
} }
} }
// Отображаем reply если есть // Отображаем reply если есть
if let Some(reply) = &msg.reply_to { if let Some(reply) = &msg.reply_to {
let reply_text: String = reply.text.chars().take(40).collect(); let reply_text: String = reply.text.chars().take(40).collect();
let ellipsis = if reply.text.chars().count() > 40 { "..." } else { "" }; let ellipsis = if reply.text.chars().count() > 40 {
"..."
} else {
""
};
let reply_line = format!("{}: {}{}", reply.sender_name, reply_text, ellipsis); let reply_line = format!("{}: {}{}", reply.sender_name, reply_text, ellipsis);
let reply_len = reply_line.chars().count(); let reply_len = reply_line.chars().count();
@@ -562,9 +590,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
])); ]));
} else { } else {
// Reply слева для входящих // Reply слева для входящих
lines.push(Line::from(vec![ lines.push(Line::from(vec![Span::styled(
Span::styled(reply_line, Style::default().fg(Color::Cyan)), reply_line,
])); Style::default().fg(Color::Cyan),
)]));
} }
} }
@@ -593,11 +622,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
); );
// Форматируем текст с entities // Форматируем текст с entities
let formatted_spans = format_text_with_entities( let formatted_spans =
&wrapped.text, format_text_with_entities(&wrapped.text, &line_entities, msg_color);
&line_entities,
msg_color,
);
if is_last_line { if is_last_line {
// Последняя строка — добавляем time_mark // Последняя строка — добавляем time_mark
@@ -605,17 +631,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
let padding = content_width.saturating_sub(full_len + 1); let padding = content_width.saturating_sub(full_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))]; let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if is_selected { if is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))); line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} }
line_spans.extend(formatted_spans); line_spans.extend(formatted_spans);
line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray))); line_spans.push(Span::styled(
format!(" {}", time_mark),
Style::default().fg(Color::Gray),
));
lines.push(Line::from(line_spans)); lines.push(Line::from(line_spans));
} else { } else {
// Промежуточные строки — просто текст справа // Промежуточные строки — просто текст справа
let padding = content_width.saturating_sub(line_len + marker_len + 1); let padding = content_width.saturating_sub(line_len + marker_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))]; let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if i == 0 && is_selected { if i == 0 && is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))); line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} }
line_spans.extend(formatted_spans); line_spans.extend(formatted_spans);
lines.push(Line::from(line_spans)); lines.push(Line::from(line_spans));
@@ -643,19 +682,24 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
); );
// Форматируем текст с entities // Форматируем текст с entities
let formatted_spans = format_text_with_entities( let formatted_spans =
&wrapped.text, format_text_with_entities(&wrapped.text, &line_entities, msg_color);
&line_entities,
msg_color,
);
if i == 0 { if i == 0 {
// Первая строка — с временем и маркером выбора // Первая строка — с временем и маркером выбора
let mut line_spans = vec![]; let mut line_spans = vec![];
if is_selected { if is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))); line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} }
line_spans.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray))); line_spans.push(Span::styled(
format!(" {}", time_str),
Style::default().fg(Color::Gray),
));
line_spans.push(Span::raw(" ")); line_spans.push(Span::raw(" "));
line_spans.extend(formatted_spans); line_spans.extend(formatted_spans);
lines.push(Line::from(line_spans)); lines.push(Line::from(line_spans));
@@ -694,9 +738,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}; };
let style = if reaction.is_chosen { let style = if reaction.is_chosen {
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_chosen)) Style::default()
.fg(app.config.parse_color(&app.config.colors.reaction_chosen))
} else { } else {
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_other)) Style::default()
.fg(app.config.parse_color(&app.config.colors.reaction_other))
}; };
reaction_spans.push(Span::styled(reaction_text, style)); reaction_spans.push(Span::styled(reaction_text, style));
@@ -723,10 +769,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} }
if lines.is_empty() { if lines.is_empty() {
lines.push(Line::from(Span::styled( lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray))));
"Нет сообщений",
Style::default().fg(Color::Gray),
)));
} }
// Вычисляем скролл с учётом пользовательского offset // Вычисляем скролл с учётом пользовательского offset
@@ -769,10 +812,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Input box с wrap для длинного текста и блочным курсором // Input box с wrap для длинного текста и блочным курсором
let (input_line, input_title) = if app.is_forwarding() { let (input_line, input_title) = if app.is_forwarding() {
// Режим пересылки - показываем превью сообщения // Режим пересылки - показываем превью сообщения
let forward_preview = app.get_forwarding_message() let forward_preview = app
.get_forwarding_message()
.map(|m| { .map(|m| {
let text_preview: String = m.content.chars().take(40).collect(); let text_preview: String = m.content.chars().take(40).collect();
let ellipsis = if m.content.chars().count() > 40 { "..." } else { "" }; let ellipsis = if m.content.chars().count() > 40 {
"..."
} else {
""
};
format!("{}{}", text_preview, ellipsis) format!("{}{}", text_preview, ellipsis)
}) })
.unwrap_or_else(|| "↪ ...".to_string()); .unwrap_or_else(|| "↪ ...".to_string());
@@ -782,8 +830,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} else if app.is_selecting_message() { } else if app.is_selecting_message() {
// Режим выбора сообщения - подсказка зависит от возможностей // Режим выбора сообщения - подсказка зависит от возможностей
let selected_msg = app.get_selected_message(); let selected_msg = app.get_selected_message();
let can_edit = selected_msg.map(|m| m.can_be_edited && m.is_outgoing).unwrap_or(false); let can_edit = selected_msg
let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false); .map(|m| m.can_be_edited && m.is_outgoing)
.unwrap_or(false);
let can_delete = selected_msg
.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users)
.unwrap_or(false);
let hint = match (can_edit, can_delete) { let hint = match (can_edit, can_delete) {
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc", (true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc",
@@ -791,7 +843,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc", (false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc",
(false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc", (false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc",
}; };
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ") (
Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))),
" Выбор сообщения ",
)
} else if app.is_editing() { } else if app.is_editing() {
// Режим редактирования // Режим редактирования
if app.message_input.is_empty() { if app.message_input.is_empty() {
@@ -804,16 +859,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(line, " Редактирование (Esc отмена) ") (line, " Редактирование (Esc отмена) ")
} else { } else {
// Текст с курсором // Текст с курсором
let line = render_input_with_cursor("", &app.message_input, app.cursor_position, Color::Magenta); let line = render_input_with_cursor(
"",
&app.message_input,
app.cursor_position,
Color::Magenta,
);
(line, " Редактирование (Esc отмена) ") (line, " Редактирование (Esc отмена) ")
} }
} else if app.is_replying() { } else if app.is_replying() {
// Режим ответа на сообщение // Режим ответа на сообщение
let reply_preview = app.get_replying_to_message() let reply_preview = app
.get_replying_to_message()
.map(|m| { .map(|m| {
let sender = if m.is_outgoing { "Вы" } else { &m.sender_name }; let sender = if m.is_outgoing {
"Вы"
} else {
&m.sender_name
};
let text_preview: String = m.content.chars().take(30).collect(); let text_preview: String = m.content.chars().take(30).collect();
let ellipsis = if m.content.chars().count() > 30 { "..." } else { "" }; let ellipsis = if m.content.chars().count() > 30 {
"..."
} else {
""
};
format!("{}: {}{}", sender, text_preview, ellipsis) format!("{}: {}{}", sender, text_preview, ellipsis)
}) })
.unwrap_or_else(|| "...".to_string()); .unwrap_or_else(|| "...".to_string());
@@ -829,7 +898,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} else { } else {
let short_preview: String = reply_preview.chars().take(15).collect(); let short_preview: String = reply_preview.chars().take(15).collect();
let prefix = format!("{} > ", short_preview); let prefix = format!("{} > ", short_preview);
let line = render_input_with_cursor(&prefix, &app.message_input, app.cursor_position, Color::Yellow); let line = render_input_with_cursor(
&prefix,
&app.message_input,
app.cursor_position,
Color::Yellow,
);
(line, " Ответ (Esc отмена) ") (line, " Ответ (Esc отмена) ")
} }
} else { } else {
@@ -844,7 +918,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(line, "") (line, "")
} else { } else {
// Текст с курсором // Текст с курсором
let line = render_input_with_cursor("> ", &app.message_input, app.cursor_position, Color::Yellow); let line = render_input_with_cursor(
"> ",
&app.message_input,
app.cursor_position,
Color::Yellow,
);
(line, "") (line, "")
} }
}; };
@@ -860,7 +939,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(input_title) .title(input_title)
.title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) .title_style(
Style::default()
.fg(title_color)
.add_modifier(Modifier::BOLD),
)
}; };
let input = Paragraph::new(input_line) let input = Paragraph::new(input_line)
@@ -882,7 +965,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Модалка выбора реакции // Модалка выбора реакции
if app.is_reaction_picker_mode() { if app.is_reaction_picker_mode() {
render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index); render_reaction_picker_modal(
f,
area,
&app.available_reactions,
app.selected_reaction_index,
);
} }
} }
@@ -899,8 +987,12 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
// Search input // Search input
let total = app.message_search_results.len(); let total = app.message_search_results.len();
let current = if total > 0 { app.selected_search_result_index + 1 } else { 0 }; let current = if total > 0 {
app.selected_search_result_index + 1
} else {
0
};
let input_line = if app.message_search_query.is_empty() { let input_line = if app.message_search_query.is_empty() {
Line::from(vec![ Line::from(vec![
Span::styled("🔍 ", Style::default().fg(Color::Yellow)), Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
@@ -915,15 +1007,18 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)), Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)),
]) ])
}; };
let search_input = Paragraph::new(input_line) let search_input = Paragraph::new(input_line).block(
.block( Block::default()
Block::default() .borders(Borders::ALL)
.borders(Borders::ALL) .border_style(Style::default().fg(Color::Yellow))
.border_style(Style::default().fg(Color::Yellow)) .title(" Поиск по сообщениям ")
.title(" Поиск по сообщениям ") .title_style(
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) Style::default()
); .fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
);
f.render_widget(search_input, chunks[0]); f.render_widget(search_input, chunks[0]);
// Search results // Search results
@@ -948,14 +1043,29 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
// Маркер выбора, имя и дата // Маркер выбора, имя и дата
let marker = if is_selected { "" } else { " " }; let marker = if is_selected { "" } else { " " };
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan }; let sender_color = if msg.is_outgoing {
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() }; Color::Green
} else {
Color::Cyan
};
let sender_name = if msg.is_outgoing {
"Вы".to_string()
} else {
msg.sender_name.clone()
};
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled( Span::styled(
format!("{} ", sender_name), format!("{} ", sender_name),
Style::default().fg(sender_color).add_modifier(Modifier::BOLD), Style::default()
.fg(sender_color)
.add_modifier(Modifier::BOLD),
), ),
Span::styled( Span::styled(
format!("({})", crate::utils::format_datetime(msg.date)), format!("({})", crate::utils::format_datetime(msg.date)),
@@ -964,7 +1074,11 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
])); ]));
// Текст сообщения (с переносом) // Текст сообщения (с переносом)
let msg_color = if is_selected { Color::Yellow } else { Color::White }; let msg_color = if is_selected {
Color::Yellow
} else {
Color::White
};
let max_width = content_width.saturating_sub(4); let max_width = content_width.saturating_sub(4);
let wrapped = wrap_text_with_offsets(&msg.content, max_width); let wrapped = wrap_text_with_offsets(&msg.content, max_width);
let wrapped_count = wrapped.len(); let wrapped_count = wrapped.len();
@@ -998,20 +1112,35 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)) .border_style(Style::default().fg(Color::Yellow)),
) )
.scroll((scroll_offset, 0)); .scroll((scroll_offset, 0));
f.render_widget(results_widget, chunks[1]); f.render_widget(results_widget, chunks[1]);
// Help bar // Help bar
let help_line = Line::from(vec![ let help_line = Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"), Span::raw("навигация"),
Span::raw(" "), Span::raw(" "),
Span::styled(" n/N ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
" n/N ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("след./пред."), Span::raw("след./пред."),
Span::raw(" "), Span::raw(" "),
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("перейти"), Span::raw("перейти"),
Span::raw(" "), Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1021,7 +1150,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)) .border_style(Style::default().fg(Color::Yellow)),
) )
.alignment(Alignment::Center); .alignment(Alignment::Center);
f.render_widget(help, chunks[2]); f.render_widget(help, chunks[2]);
@@ -1046,9 +1175,13 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)) .border_style(Style::default().fg(Color::Magenta)),
) )
.style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)); .style(
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
);
f.render_widget(header, chunks[0]); f.render_widget(header, chunks[0]);
// Pinned messages list // Pinned messages list
@@ -1057,7 +1190,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
for (idx, msg) in app.pinned_messages.iter().enumerate() { for (idx, msg) in app.pinned_messages.iter().enumerate() {
let is_selected = idx == app.selected_pinned_index; let is_selected = idx == app.selected_pinned_index;
// Пустая строка между сообщениями // Пустая строка между сообщениями
if idx > 0 { if idx > 0 {
lines.push(Line::from("")); lines.push(Line::from(""));
@@ -1065,14 +1198,29 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
// Маркер выбора и имя отправителя // Маркер выбора и имя отправителя
let marker = if is_selected { "" } else { " " }; let marker = if is_selected { "" } else { " " };
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan }; let sender_color = if msg.is_outgoing {
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() }; Color::Green
} else {
Color::Cyan
};
let sender_name = if msg.is_outgoing {
"Вы".to_string()
} else {
msg.sender_name.clone()
};
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled( Span::styled(
format!("{} ", sender_name), format!("{} ", sender_name),
Style::default().fg(sender_color).add_modifier(Modifier::BOLD), Style::default()
.fg(sender_color)
.add_modifier(Modifier::BOLD),
), ),
Span::styled( Span::styled(
format!("({})", crate::utils::format_datetime(msg.date)), format!("({})", crate::utils::format_datetime(msg.date)),
@@ -1081,12 +1229,17 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
])); ]));
// Текст сообщения (с переносом) // Текст сообщения (с переносом)
let msg_color = if is_selected { Color::Yellow } else { Color::White }; let msg_color = if is_selected {
Color::Yellow
} else {
Color::White
};
let max_width = content_width.saturating_sub(4); let max_width = content_width.saturating_sub(4);
let wrapped = wrap_text_with_offsets(&msg.content, max_width); let wrapped = wrap_text_with_offsets(&msg.content, max_width);
let wrapped_count = wrapped.len(); let wrapped_count = wrapped.len();
for wrapped_line in wrapped.into_iter().take(3) { // Максимум 3 строки на сообщение for wrapped_line in wrapped.into_iter().take(3) {
// Максимум 3 строки на сообщение
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::raw(" "), // Отступ Span::raw(" "), // Отступ
Span::styled(wrapped_line.text, Style::default().fg(msg_color)), Span::styled(wrapped_line.text, Style::default().fg(msg_color)),
@@ -1121,17 +1274,27 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)) .border_style(Style::default().fg(Color::Magenta)),
) )
.scroll((scroll_offset, 0)); .scroll((scroll_offset, 0));
f.render_widget(messages_widget, chunks[1]); f.render_widget(messages_widget, chunks[1]);
// Help bar // Help bar
let help_line = Line::from(vec![ let help_line = Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"), Span::raw("навигация"),
Span::raw(" "), Span::raw(" "),
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("перейти"), Span::raw("перейти"),
Span::raw(" "), Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1141,7 +1304,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)) .border_style(Style::default().fg(Color::Magenta)),
) )
.alignment(Alignment::Center); .alignment(Alignment::Center);
f.render_widget(help, chunks[2]); f.render_widget(help, chunks[2]);
@@ -1169,11 +1332,18 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
Line::from(""), Line::from(""),
Line::from(Span::styled( Line::from(Span::styled(
"Удалить сообщение?", "Удалить сообщение?",
Style::default().fg(Color::White).add_modifier(Modifier::BOLD), Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)), )),
Line::from(""), Line::from(""),
Line::from(vec![ Line::from(vec![
Span::styled(" [y/Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::styled(
" [y/Enter] ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("Да"), Span::raw("Да"),
Span::raw(" "), Span::raw(" "),
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1194,9 +1364,13 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
f.render_widget(modal, modal_area); f.render_widget(modal, modal_area);
} }
/// Рендерит модалку выбора реакции /// Рендерит модалку выбора реакции
fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) { fn render_reaction_picker_modal(
f: &mut Frame,
area: Rect,
available_reactions: &[String],
selected_index: usize,
) {
use ratatui::widgets::Clear; use ratatui::widgets::Clear;
// Размеры модалки (зависят от количества реакций) // Размеры модалки (зависят от количества реакций)
@@ -1248,9 +1422,19 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
// Добавляем пустую строку и подсказку // Добавляем пустую строку и подсказку
text_lines.push(Line::from("")); text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![ text_lines.push(Line::from(vec![
Span::styled(" [←/→/↑/↓] ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), Span::styled(
" [←/→/↑/↓] ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw("Выбор "), Span::raw("Выбор "),
Span::styled(" [Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), 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::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw("Отмена"), Span::raw("Отмена"),
@@ -1262,7 +1446,11 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)) .border_style(Style::default().fg(Color::Yellow))
.title(" Выбери реакцию ") .title(" Выбери реакцию ")
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), .title_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
) )
.alignment(Alignment::Left); .alignment(Alignment::Left);

View File

@@ -1,16 +1,16 @@
mod loading;
mod auth; mod auth;
mod main_screen;
pub mod chat_list; pub mod chat_list;
pub mod messages;
pub mod footer; pub mod footer;
mod loading;
mod main_screen;
pub mod messages;
pub mod profile; pub mod profile;
use ratatui::Frame; use crate::app::{App, AppScreen};
use ratatui::layout::Alignment; use ratatui::layout::Alignment;
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use crate::app::{App, AppScreen}; use ratatui::Frame;
/// Минимальная высота терминала /// Минимальная высота терминала
const MIN_HEIGHT: u16 = 10; const MIN_HEIGHT: u16 = 10;
@@ -34,12 +34,13 @@ pub fn render(f: &mut Frame, app: &mut App) {
} }
fn render_size_warning(f: &mut Frame, width: u16, height: u16) { fn render_size_warning(f: &mut Frame, width: u16, height: u16) {
let message = format!( let message = format!("{}x{}\nМинимум: {}x{}", width, height, MIN_WIDTH, MIN_HEIGHT);
"{}x{}\nМинимум: {}x{}",
width, height, MIN_WIDTH, MIN_HEIGHT
);
let warning = Paragraph::new(message) let warning = Paragraph::new(message)
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) .style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.alignment(Alignment::Center); .alignment(Alignment::Center);
f.render_widget(warning, f.area()); f.render_widget(warning, f.area());
} }

View File

@@ -1,3 +1,5 @@
use crate::app::App;
use crate::tdlib::client::ProfileInfo;
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
@@ -5,8 +7,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use crate::app::App;
use crate::tdlib::client::ProfileInfo;
/// Рендерит режим просмотра профиля /// Рендерит режим просмотра профиля
pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
@@ -20,9 +20,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Length(3), // Header Constraint::Length(3), // Header
Constraint::Min(0), // Profile info Constraint::Min(0), // Profile info
Constraint::Length(3), // Actions help Constraint::Length(3), // Actions help
]) ])
.split(area); .split(area);
@@ -32,9 +32,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)) .border_style(Style::default().fg(Color::Cyan)),
) )
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); .style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
f.render_widget(header, chunks[0]); f.render_widget(header, chunks[0]);
// Profile info // Profile info
@@ -83,9 +87,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
// Bio (только для личных чатов) // Bio (только для личных чатов)
if let Some(bio) = &profile.bio { if let Some(bio) = &profile.bio {
lines.push(Line::from(vec![ lines.push(Line::from(vec![Span::styled("О себе: ", Style::default().fg(Color::Gray))]));
Span::styled("О себе: ", Style::default().fg(Color::Gray)),
]));
// Разбиваем bio на строки если длинное // Разбиваем bio на строки если длинное
let bio_lines: Vec<&str> = bio.lines().collect(); let bio_lines: Vec<&str> = bio.lines().collect();
for bio_line in bio_lines { for bio_line in bio_lines {
@@ -105,9 +107,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
// Description (для групп/каналов) // Description (для групп/каналов)
if let Some(desc) = &profile.description { if let Some(desc) = &profile.description {
lines.push(Line::from(vec![ lines.push(Line::from(vec![Span::styled("Описание: ", Style::default().fg(Color::Gray))]));
Span::styled("Описание: ", Style::default().fg(Color::Gray)),
]));
let desc_lines: Vec<&str> = desc.lines().collect(); let desc_lines: Vec<&str> = desc.lines().collect();
for desc_line in desc_lines { for desc_line in desc_lines {
lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White)))); lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White))));
@@ -119,7 +119,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
if let Some(link) = &profile.invite_link { if let Some(link) = &profile.invite_link {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled("Ссылка: ", Style::default().fg(Color::Gray)), Span::styled("Ссылка: ", Style::default().fg(Color::Gray)),
Span::styled(link, Style::default().fg(Color::Blue).add_modifier(Modifier::UNDERLINED)), Span::styled(
link,
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::UNDERLINED),
),
])); ]));
lines.push(Line::from("")); lines.push(Line::from(""));
} }
@@ -131,7 +136,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
// Действия // Действия
lines.push(Line::from(Span::styled( lines.push(Line::from(Span::styled(
"Действия:", "Действия:",
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
))); )));
lines.push(Line::from("")); lines.push(Line::from(""));
@@ -140,7 +147,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
let is_selected = idx == app.selected_profile_action; let is_selected = idx == app.selected_profile_action;
let marker = if is_selected { "" } else { " " }; let marker = if is_selected { "" } else { " " };
let style = if is_selected { let style = if is_selected {
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else { } else {
Style::default().fg(Color::White) Style::default().fg(Color::White)
}; };
@@ -154,17 +163,27 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)) .border_style(Style::default().fg(Color::Cyan)),
) )
.scroll((0, 0)); .scroll((0, 0));
f.render_widget(info_widget, chunks[1]); f.render_widget(info_widget, chunks[1]);
// Help bar // Help bar
let help_line = Line::from(vec![ let help_line = Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"), Span::raw("навигация"),
Span::raw(" "), Span::raw(" "),
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("выбрать"), Span::raw("выбрать"),
Span::raw(" "), Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -174,7 +193,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)) .border_style(Style::default().fg(Color::Cyan)),
) )
.alignment(Alignment::Center); .alignment(Alignment::Center);
f.render_widget(help, chunks[2]); f.render_widget(help, chunks[2]);
@@ -183,17 +202,17 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
/// Получить список доступных действий /// Получить список доступных действий
fn get_available_actions(profile: &ProfileInfo) -> Vec<&'static str> { fn get_available_actions(profile: &ProfileInfo) -> Vec<&'static str> {
let mut actions = vec![]; let mut actions = vec![];
if profile.username.is_some() { if profile.username.is_some() {
actions.push("Открыть в браузере"); actions.push("Открыть в браузере");
} }
actions.push("Скопировать ID"); actions.push("Скопировать ID");
if profile.is_group { if profile.is_group {
actions.push("Покинуть группу"); actions.push("Покинуть группу");
} }
actions actions
} }
@@ -212,12 +231,19 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
Line::from(""), Line::from(""),
Line::from(Span::styled( Line::from(Span::styled(
text, text,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)), )),
Line::from(""), Line::from(""),
Line::from(""), Line::from(""),
Line::from(vec![ Line::from(vec![
Span::styled("y/н/Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::styled(
"y/н/Enter",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw(" — да "), Span::raw(" — да "),
Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw(" — нет"), Span::raw(" — нет"),
@@ -230,7 +256,7 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red)) .border_style(Style::default().fg(Color::Red))
.title(" ⚠ ВНИМАНИЕ ") .title(" ⚠ ВНИМАНИЕ ")
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
) )
.alignment(Alignment::Center); .alignment(Alignment::Center);

View File

@@ -105,21 +105,21 @@ pub fn get_day(timestamp: i32) -> i64 {
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM) /// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
pub fn format_datetime(timestamp: i32) -> String { pub fn format_datetime(timestamp: i32) -> String {
let secs = timestamp as i64; let secs = timestamp as i64;
// Время // Время
let hours = ((secs % 86400) / 3600) as u32; let hours = ((secs % 86400) / 3600) as u32;
let minutes = ((secs % 3600) / 60) as u32; let minutes = ((secs % 3600) / 60) as u32;
let local_hours = (hours + 3) % 24; // MSK let local_hours = (hours + 3) % 24; // MSK
// Дата // Дата
let days_since_epoch = secs / 86400; let days_since_epoch = secs / 86400;
let year = 1970 + (days_since_epoch / 365) as i32; let year = 1970 + (days_since_epoch / 365) as i32;
let day_of_year = days_since_epoch % 365; let day_of_year = days_since_epoch % 365;
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1; let mut month = 1;
let mut day = day_of_year as i32; let mut day = day_of_year as i32;
for (i, &m) in months.iter().enumerate() { for (i, &m) in months.iter().enumerate() {
if day < m { if day < m {
month = i + 1; month = i + 1;
@@ -127,7 +127,7 @@ pub fn format_datetime(timestamp: i32) -> String {
} }
day -= m; day -= m;
} }
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes) format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
} }

View File

@@ -2,9 +2,9 @@
mod helpers; mod helpers;
use helpers::test_data::{TestChatBuilder, create_test_chat};
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::{create_test_chat, TestChatBuilder};
use insta::assert_snapshot; use insta::assert_snapshot;
#[test] #[test]
@@ -44,9 +44,7 @@ fn snapshot_chat_with_unread_count() {
.last_message("Привет, как дела?") .last_message("Привет, как дела?")
.build(); .build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app); tele_tui::ui::chat_list::render(f, f.area(), &mut app);
@@ -63,9 +61,7 @@ fn snapshot_chat_with_pinned() {
.last_message("Pinned message") .last_message("Pinned message")
.build(); .build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app); tele_tui::ui::chat_list::render(f, f.area(), &mut app);
@@ -83,9 +79,7 @@ fn snapshot_chat_with_muted() {
.last_message("Too many messages") .last_message("Too many messages")
.build(); .build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app); tele_tui::ui::chat_list::render(f, f.area(), &mut app);
@@ -103,9 +97,7 @@ fn snapshot_chat_with_mentions() {
.last_message("@me check this out") .last_message("@me check this out")
.build(); .build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app); tele_tui::ui::chat_list::render(f, f.area(), &mut app);
@@ -139,9 +131,7 @@ fn snapshot_chat_long_title() {
.last_message("Test message") .last_message("Test message")
.build(); .build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app); tele_tui::ui::chat_list::render(f, f.area(), &mut app);

View File

@@ -61,9 +61,7 @@ fn test_can_only_delete_own_messages_for_all() {
let mut client = FakeTdClient::new(); let mut client = FakeTdClient::new();
// Наше исходящее сообщение (можно удалить для всех) // Наше исходящее сообщение (можно удалить для всех)
let outgoing_msg = TestMessageBuilder::new("My message", 1) let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build();
.outgoing()
.build();
client = client.with_message(123, outgoing_msg); client = client.with_message(123, outgoing_msg);
@@ -76,7 +74,7 @@ fn test_can_only_delete_own_messages_for_all() {
// Проверяем флаги удаления // Проверяем флаги удаления
let messages = client.get_messages(123); let messages = client.get_messages(123);
assert_eq!(messages[0].can_be_deleted_for_all_users, true); // Наше assert_eq!(messages[0].can_be_deleted_for_all_users, true); // Наше
assert_eq!(messages[1].can_be_deleted_for_all_users, false); // Чужое assert_eq!(messages[1].can_be_deleted_for_all_users, false); // Чужое
// Оба можно удалить для себя // Оба можно удалить для себя

View File

@@ -12,9 +12,7 @@ struct DraftManager {
impl DraftManager { impl DraftManager {
fn new() -> Self { fn new() -> Self {
Self { Self { drafts: HashMap::new() }
drafts: HashMap::new(),
}
} }
/// Сохранить черновик для чата /// Сохранить черновик для чата

View File

@@ -55,9 +55,7 @@ fn test_can_only_edit_own_messages() {
let mut client = FakeTdClient::new(); let mut client = FakeTdClient::new();
// Наше исходящее сообщение (можно редактировать) // Наше исходящее сообщение (можно редактировать)
let outgoing_msg = TestMessageBuilder::new("My message", 1) let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build();
.outgoing()
.build();
client = client.with_message(123, outgoing_msg); client = client.with_message(123, outgoing_msg);
@@ -70,7 +68,7 @@ fn test_can_only_edit_own_messages() {
// Проверяем флаги // Проверяем флаги
let messages = client.get_messages(123); let messages = client.get_messages(123);
assert_eq!(messages[0].can_be_edited, true); // Наше сообщение assert_eq!(messages[0].can_be_edited, true); // Наше сообщение
assert_eq!(messages[1].can_be_edited, false); // Чужое сообщение assert_eq!(messages[1].can_be_edited, false); // Чужое сообщение
} }

View File

@@ -2,9 +2,9 @@
mod helpers; mod helpers;
use helpers::test_data::create_test_chat;
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::create_test_chat;
use insta::assert_snapshot; use insta::assert_snapshot;
use tele_tui::tdlib::NetworkState; use tele_tui::tdlib::NetworkState;
@@ -12,9 +12,7 @@ use tele_tui::tdlib::NetworkState;
fn snapshot_footer_chat_list() { fn snapshot_footer_chat_list() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let app = TestAppBuilder::new() let app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::footer::render(f, f.area(), &app); tele_tui::ui::footer::render(f, f.area(), &app);
@@ -45,9 +43,7 @@ fn snapshot_footer_open_chat() {
fn snapshot_footer_network_waiting() { fn snapshot_footer_network_waiting() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
// Set network state to WaitingForNetwork // Set network state to WaitingForNetwork
app.td_client.network_state = NetworkState::WaitingForNetwork; app.td_client.network_state = NetworkState::WaitingForNetwork;
@@ -64,9 +60,7 @@ fn snapshot_footer_network_waiting() {
fn snapshot_footer_network_connecting_proxy() { fn snapshot_footer_network_connecting_proxy() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
// Set network state to ConnectingToProxy // Set network state to ConnectingToProxy
app.td_client.network_state = NetworkState::ConnectingToProxy; app.td_client.network_state = NetworkState::ConnectingToProxy;
@@ -83,9 +77,7 @@ fn snapshot_footer_network_connecting_proxy() {
fn snapshot_footer_network_connecting() { fn snapshot_footer_network_connecting() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().with_chat(chat).build();
.with_chat(chat)
.build();
// Set network state to Connecting // Set network state to Connecting
app.td_client.network_state = NetworkState::Connecting; app.td_client.network_state = NetworkState::Connecting;

View File

@@ -1,11 +1,11 @@
// Test App builder // Test App builder
use tele_tui::app::{App, AppScreen};
use tele_tui::config::Config;
use tele_tui::tdlib::{ChatInfo, MessageInfo};
use tele_tui::tdlib::client::AuthState;
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use std::collections::HashMap; use std::collections::HashMap;
use tele_tui::app::{App, AppScreen};
use tele_tui::config::Config;
use tele_tui::tdlib::client::AuthState;
use tele_tui::tdlib::{ChatInfo, MessageInfo};
/// Builder для создания тестового App /// Builder для создания тестового App
/// ///
@@ -149,13 +149,19 @@ impl TestAppBuilder {
/// Добавить сообщение для чата /// Добавить сообщение для чата
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self { pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages.entry(chat_id).or_insert_with(Vec::new).push(message); self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.push(message);
self self
} }
/// Добавить несколько сообщений для чата /// Добавить несколько сообщений для чата
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self { pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages.entry(chat_id).or_insert_with(Vec::new).extend(messages); self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.extend(messages);
self self
} }
@@ -329,9 +335,7 @@ mod tests {
#[test] #[test]
fn test_builder_search_mode() { fn test_builder_search_mode() {
let app = TestAppBuilder::new() let app = TestAppBuilder::new().searching("test query").build();
.searching("test query")
.build();
assert!(app.is_searching); assert!(app.is_searching);
assert_eq!(app.search_query, "test query"); assert_eq!(app.search_query, "test query");

View File

@@ -1,7 +1,7 @@
// Fake TDLib client for testing // Fake TDLib client for testing
use std::collections::HashMap; use std::collections::HashMap;
use tele_tui::tdlib::{ChatInfo, MessageInfo, FolderInfo, NetworkState}; use tele_tui::tdlib::{ChatInfo, FolderInfo, MessageInfo, NetworkState};
/// Упрощённый mock TDLib клиента для тестов /// Упрощённый mock TDLib клиента для тестов
#[derive(Clone)] #[derive(Clone)]
@@ -42,12 +42,7 @@ impl FakeTdClient {
Self { Self {
chats: vec![], chats: vec![],
messages: HashMap::new(), messages: HashMap::new(),
folders: vec![ folders: vec![FolderInfo { id: 0, name: "All".to_string() }],
FolderInfo {
id: 0,
name: "All".to_string(),
},
],
user_names: HashMap::new(), user_names: HashMap::new(),
network_state: NetworkState::Ready, network_state: NetworkState::Ready,
typing_chat_id: None, typing_chat_id: None,
@@ -90,10 +85,7 @@ impl FakeTdClient {
/// Добавить папку /// Добавить папку
pub fn with_folder(mut self, id: i32, name: &str) -> Self { pub fn with_folder(mut self, id: i32, name: &str) -> Self {
self.folders.push(FolderInfo { self.folders.push(FolderInfo { id, name: name.to_string() });
id,
name: name.to_string(),
});
self self
} }
@@ -116,10 +108,7 @@ impl FakeTdClient {
/// Получить сообщения для чата /// Получить сообщения для чата
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> { pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
self.messages self.messages.get(&chat_id).cloned().unwrap_or_default()
.get(&chat_id)
.cloned()
.unwrap_or_default()
} }
/// Получить папки /// Получить папки
@@ -131,11 +120,8 @@ impl FakeTdClient {
pub fn send_message(&mut self, chat_id: i64, text: String, reply_to: Option<i64>) -> i64 { pub fn send_message(&mut self, chat_id: i64, text: String, reply_to: Option<i64>) -> i64 {
let message_id = (self.sent_messages.len() as i64) + 1000; let message_id = (self.sent_messages.len() as i64) + 1000;
self.sent_messages.push(SentMessage { self.sent_messages
chat_id, .push(SentMessage { chat_id, text: text.clone(), reply_to });
text: text.clone(),
reply_to,
});
// Добавляем сообщение в список сообщений чата // Добавляем сообщение в список сообщений чата
let message = MessageInfo { let message = MessageInfo {
@@ -165,10 +151,8 @@ impl FakeTdClient {
/// Редактировать сообщение (мок) /// Редактировать сообщение (мок)
pub fn edit_message(&mut self, chat_id: i64, message_id: i64, new_text: String) { pub fn edit_message(&mut self, chat_id: i64, message_id: i64, new_text: String) {
self.edited_messages.push(EditedMessage { self.edited_messages
message_id, .push(EditedMessage { message_id, new_text: new_text.clone() });
new_text: new_text.clone(),
});
// Обновляем сообщение в списке // Обновляем сообщение в списке
if let Some(messages) = self.messages.get_mut(&chat_id) { if let Some(messages) = self.messages.get_mut(&chat_id) {

View File

@@ -1,9 +1,9 @@
// Snapshot testing utilities // Snapshot testing utilities
use ratatui::backend::TestBackend; use ratatui::backend::TestBackend;
use ratatui::Terminal;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::Terminal;
/// Конвертирует Buffer в читаемую строку для snapshot тестов /// Конвертирует Buffer в читаемую строку для snapshot тестов
pub fn buffer_to_string(buffer: &Buffer) -> String { pub fn buffer_to_string(buffer: &Buffer) -> String {
@@ -33,9 +33,7 @@ where
let backend = TestBackend::new(width, height); let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
terminal terminal.draw(render_fn).unwrap();
.draw(render_fn)
.unwrap();
terminal.backend().buffer().clone() terminal.backend().buffer().clone()
} }
@@ -44,7 +42,7 @@ where
#[macro_export] #[macro_export]
macro_rules! assert_ui_snapshot { macro_rules! assert_ui_snapshot {
($name:expr, $width:expr, $height:expr, $render_fn:expr) => {{ ($name:expr, $width:expr, $height:expr, $render_fn:expr) => {{
use $crate::helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use $crate::helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
let buffer = render_to_buffer($width, $height, $render_fn); let buffer = render_to_buffer($width, $height, $render_fn);
let output = buffer_to_string(&buffer); let output = buffer_to_string(&buffer);
insta::assert_snapshot!($name, output); insta::assert_snapshot!($name, output);
@@ -59,9 +57,7 @@ mod tests {
#[test] #[test]
fn test_buffer_to_string_simple() { fn test_buffer_to_string_simple() {
let buffer = render_to_buffer(10, 3, |f| { let buffer = render_to_buffer(10, 3, |f| {
let block = Block::default() let block = Block::default().borders(Borders::ALL).title("Hi");
.borders(Borders::ALL)
.title("Hi");
f.render_widget(block, f.area()); f.render_widget(block, f.area());
}); });

View File

@@ -1,6 +1,6 @@
// Test data builders and fixtures // Test data builders and fixtures
use tele_tui::tdlib::{ChatInfo, MessageInfo, ReactionInfo, ReplyInfo, ForwardInfo, ProfileInfo}; use tele_tui::tdlib::{ChatInfo, ForwardInfo, MessageInfo, ProfileInfo, ReactionInfo, ReplyInfo};
/// Builder для создания тестового чата /// Builder для создания тестового чата
pub struct TestChatBuilder { pub struct TestChatBuilder {
@@ -181,11 +181,8 @@ impl TestMessageBuilder {
} }
pub fn reaction(mut self, emoji: &str, count: i32, chosen: bool) -> Self { pub fn reaction(mut self, emoji: &str, count: i32, chosen: bool) -> Self {
self.reactions.push(ReactionInfo { self.reactions
emoji: emoji.to_string(), .push(ReactionInfo { emoji: emoji.to_string(), count, is_chosen: chosen });
count,
is_chosen: chosen,
});
self self
} }

View File

@@ -2,9 +2,9 @@
mod helpers; mod helpers;
use helpers::test_data::{TestMessageBuilder, create_test_chat};
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::{create_test_chat, TestMessageBuilder};
use insta::assert_snapshot; use insta::assert_snapshot;
#[test] #[test]

View File

@@ -2,9 +2,9 @@
mod helpers; mod helpers;
use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat};
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
use insta::assert_snapshot; use insta::assert_snapshot;
#[test] #[test]
@@ -48,9 +48,7 @@ fn snapshot_single_incoming_message() {
#[test] #[test]
fn snapshot_single_outgoing_message() { fn snapshot_single_outgoing_message() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Hi mom!", 1) let message = TestMessageBuilder::new("Hi mom!", 1).outgoing().build();
.outgoing()
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -122,9 +120,7 @@ fn snapshot_sender_grouping() {
#[test] #[test]
fn snapshot_outgoing_sent() { fn snapshot_outgoing_sent() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Just sent", 1) let message = TestMessageBuilder::new("Just sent", 1).outgoing().build();
.outgoing()
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -173,9 +169,7 @@ fn snapshot_outgoing_read() {
#[test] #[test]
fn snapshot_edited_message() { fn snapshot_edited_message() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Edited text", 1) let message = TestMessageBuilder::new("Edited text", 1).edited().build();
.edited()
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -195,8 +189,7 @@ fn snapshot_edited_message() {
fn snapshot_long_message_wrap() { fn snapshot_long_message_wrap() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let long_text = "This is a very long message that should wrap across multiple lines when rendered in the terminal UI. Let's make it even longer to ensure we test the wrapping behavior properly."; let long_text = "This is a very long message that should wrap across multiple lines when rendered in the terminal UI. Let's make it even longer to ensure we test the wrapping behavior properly.";
let message = TestMessageBuilder::new(long_text, 1) let message = TestMessageBuilder::new(long_text, 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -215,8 +208,7 @@ fn snapshot_long_message_wrap() {
#[test] #[test]
fn snapshot_markdown_bold_italic_code() { fn snapshot_markdown_bold_italic_code() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("**bold** *italic* `code`", 1) let message = TestMessageBuilder::new("**bold** *italic* `code`", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -235,8 +227,8 @@ fn snapshot_markdown_bold_italic_code() {
#[test] #[test]
fn snapshot_markdown_link_mention() { fn snapshot_markdown_link_mention() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Check [this](https://example.com) and @username", 1) let message =
.build(); TestMessageBuilder::new("Check [this](https://example.com) and @username", 1).build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -255,8 +247,7 @@ fn snapshot_markdown_link_mention() {
#[test] #[test]
fn snapshot_markdown_spoiler() { fn snapshot_markdown_spoiler() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1) let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -275,8 +266,7 @@ fn snapshot_markdown_spoiler() {
#[test] #[test]
fn snapshot_media_placeholder() { fn snapshot_media_placeholder() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("[Фото]", 1) let message = TestMessageBuilder::new("[Фото]", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -380,8 +370,7 @@ fn snapshot_multiple_reactions() {
#[test] #[test]
fn snapshot_selected_message() { fn snapshot_selected_message() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Selected message", 1) let message = TestMessageBuilder::new("Selected message", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)

View File

@@ -2,17 +2,17 @@
mod helpers; mod helpers;
use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat, create_test_profile};
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::{
create_test_chat, create_test_profile, TestChatBuilder, TestMessageBuilder,
};
use insta::assert_snapshot; use insta::assert_snapshot;
#[test] #[test]
fn snapshot_delete_confirmation_modal() { fn snapshot_delete_confirmation_modal() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Delete me", 1) let message = TestMessageBuilder::new("Delete me", 1).outgoing().build();
.outgoing()
.build();
let app = TestAppBuilder::new() let app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -32,8 +32,7 @@ fn snapshot_delete_confirmation_modal() {
#[test] #[test]
fn snapshot_emoji_picker_default() { fn snapshot_emoji_picker_default() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("React to this", 1) let message = TestMessageBuilder::new("React to this", 1).build();
.build();
let app = TestAppBuilder::new() let app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -53,8 +52,7 @@ fn snapshot_emoji_picker_default() {
#[test] #[test]
fn snapshot_emoji_picker_with_selection() { fn snapshot_emoji_picker_with_selection() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("React to this", 1) let message = TestMessageBuilder::new("React to this", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -97,8 +95,7 @@ fn snapshot_profile_personal_chat() {
#[test] #[test]
fn snapshot_profile_group_chat() { fn snapshot_profile_group_chat() {
let chat = TestChatBuilder::new("Work Group", 456) let chat = TestChatBuilder::new("Work Group", 456).build();
.build();
let mut profile = create_test_profile("Work Group", 456); let mut profile = create_test_profile("Work Group", 456);
profile.is_group = true; profile.is_group = true;
@@ -125,10 +122,8 @@ fn snapshot_profile_group_chat() {
#[test] #[test]
fn snapshot_pinned_message() { fn snapshot_pinned_message() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let message1 = TestMessageBuilder::new("Regular message", 1) let message1 = TestMessageBuilder::new("Regular message", 1).build();
.build(); let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2).build();
let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2)
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -150,12 +145,9 @@ fn snapshot_pinned_message() {
#[test] #[test]
fn snapshot_search_in_chat() { fn snapshot_search_in_chat() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let msg1 = TestMessageBuilder::new("Hello world", 1) let msg1 = TestMessageBuilder::new("Hello world", 1).build();
.build(); let msg2 = TestMessageBuilder::new("World is beautiful", 2).build();
let msg2 = TestMessageBuilder::new("World is beautiful", 2) let msg3 = TestMessageBuilder::new("Beautiful day", 3).build();
.build();
let msg3 = TestMessageBuilder::new("Beautiful day", 3)
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chat(chat) .with_chat(chat)
@@ -182,8 +174,7 @@ fn snapshot_forward_mode() {
let chat2 = create_test_chat("Dad", 456); let chat2 = create_test_chat("Dad", 456);
let chat3 = create_test_chat("Work Group", 789); let chat3 = create_test_chat("Work Group", 789);
let message = TestMessageBuilder::new("Forward this message", 1) let message = TestMessageBuilder::new("Forward this message", 1).build();
.build();
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.with_chats(vec![chat1.clone(), chat2, chat3]) .with_chats(vec![chat1.clone(), chat2, chat3])

View File

@@ -128,9 +128,7 @@ fn test_switch_folders() {
let mut client = FakeTdClient::new(); let mut client = FakeTdClient::new();
// Добавляем папки (FakeTdClient уже создаёт "All" с id=0) // Добавляем папки (FakeTdClient уже создаёт "All" с id=0)
client = client client = client.with_folder(1, "Personal").with_folder(2, "Work");
.with_folder(1, "Personal")
.with_folder(2, "Work");
let folders = client.get_folders(); let folders = client.get_folders();

View File

@@ -3,7 +3,7 @@
mod helpers; mod helpers;
use helpers::app_builder::TestAppBuilder; use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
use helpers::test_data::create_test_chat; use helpers::test_data::create_test_chat;
use insta::assert_snapshot; use insta::assert_snapshot;
use tele_tui::app::AppScreen; use tele_tui::app::AppScreen;
@@ -11,9 +11,7 @@ use tele_tui::tdlib::client::AuthState;
#[test] #[test]
fn snapshot_loading_screen_default() { fn snapshot_loading_screen_default() {
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().screen(AppScreen::Loading).build();
.screen(AppScreen::Loading)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::render(f, &mut app); tele_tui::ui::render(f, &mut app);
@@ -88,9 +86,7 @@ fn snapshot_auth_screen_password() {
#[test] #[test]
fn snapshot_main_screen_empty() { fn snapshot_main_screen_empty() {
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new().screen(AppScreen::Main).build();
.screen(AppScreen::Main)
.build();
let buffer = render_to_buffer(80, 24, |f| { let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::render(f, &mut app); tele_tui::ui::render(f, &mut app);
@@ -103,7 +99,7 @@ fn snapshot_main_screen_empty() {
#[test] #[test]
fn snapshot_main_screen_terminal_too_small() { fn snapshot_main_screen_terminal_too_small() {
let chat = create_test_chat("Mom", 123); let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new() let mut app = TestAppBuilder::new()
.screen(AppScreen::Main) .screen(AppScreen::Main)
.with_chat(chat) .with_chat(chat)

View File

@@ -34,13 +34,9 @@ fn test_search_chats_by_title() {
fn test_search_chats_by_username() { fn test_search_chats_by_username() {
let mut client = FakeTdClient::new(); let mut client = FakeTdClient::new();
let chat1 = TestChatBuilder::new("Alice", 123) let chat1 = TestChatBuilder::new("Alice", 123).username("alice").build();
.username("alice")
.build();
let chat2 = TestChatBuilder::new("Bob", 456) let chat2 = TestChatBuilder::new("Bob", 456).username("bobby").build();
.username("bobby")
.build();
let chat3 = TestChatBuilder::new("Charlie", 789).build(); // Без username let chat3 = TestChatBuilder::new("Charlie", 789).build(); // Без username

View File

@@ -139,7 +139,7 @@ fn test_receive_incoming_message() {
// Проверяем что в списке 2 сообщения // Проверяем что в списке 2 сообщения
let messages = client.get_messages(123); let messages = client.get_messages(123);
assert_eq!(messages.len(), 2); assert_eq!(messages.len(), 2);
assert_eq!(messages[0].is_outgoing, true); // Наше сообщение assert_eq!(messages[0].is_outgoing, true); // Наше сообщение
assert_eq!(messages[1].is_outgoing, false); // Входящее assert_eq!(messages[1].is_outgoing, false); // Входящее
assert_eq!(messages[1].content, "Hey there!"); assert_eq!(messages[1].content, "Hey there!");
assert_eq!(messages[1].sender_name, "Alice"); assert_eq!(messages[1].sender_name, "Alice");