commit
This commit is contained in:
@@ -2,9 +2,9 @@ mod state;
|
||||
|
||||
pub use state::AppScreen;
|
||||
|
||||
use ratatui::widgets::ListState;
|
||||
use crate::tdlib::client::ChatInfo;
|
||||
use crate::tdlib::TdClient;
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
pub struct App {
|
||||
pub config: crate::config::Config,
|
||||
@@ -87,7 +87,6 @@ pub struct App {
|
||||
pub selected_reaction_index: usize,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
pub fn new(config: crate::config::Config) -> App {
|
||||
let mut state = ListState::default();
|
||||
@@ -226,13 +225,14 @@ impl App {
|
||||
self.selected_message_index = Some(
|
||||
self.selected_message_index
|
||||
.map(|i| (i + 1).min(total - 1))
|
||||
.unwrap_or(0)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
}
|
||||
|
||||
/// Выбрать следующее сообщение (вниз по списку = уменьшить индекс)
|
||||
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 })
|
||||
.flatten();
|
||||
}
|
||||
@@ -312,7 +312,8 @@ impl App {
|
||||
pub fn get_filtered_chats(&self) -> Vec<&ChatInfo> {
|
||||
let folder_filtered: Vec<&ChatInfo> = match self.selected_folder_id {
|
||||
None => self.chats.iter().collect(), // All - показываем все
|
||||
Some(folder_id) => self.chats
|
||||
Some(folder_id) => self
|
||||
.chats
|
||||
.iter()
|
||||
.filter(|c| c.folder_ids.contains(&folder_id))
|
||||
.collect(),
|
||||
@@ -410,7 +411,10 @@ impl App {
|
||||
/// Получить сообщение, на которое отвечаем
|
||||
pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||
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> {
|
||||
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 (вверх = более старый)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -530,8 +539,8 @@ impl App {
|
||||
|
||||
/// Выбрать следующий результат (вниз)
|
||||
pub fn select_next_search_result(&mut self) {
|
||||
if !self.message_search_results.is_empty()
|
||||
&& self.selected_search_result_index < self.message_search_results.len() - 1
|
||||
if !self.message_search_results.is_empty()
|
||||
&& self.selected_search_result_index < self.message_search_results.len() - 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> {
|
||||
self.message_search_results.get(self.selected_search_result_index)
|
||||
self.message_search_results
|
||||
.get(self.selected_search_result_index)
|
||||
}
|
||||
|
||||
/// Получить ID выбранного результата для перехода
|
||||
@@ -629,7 +639,11 @@ impl App {
|
||||
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.selected_message_for_reaction = Some(message_id);
|
||||
self.available_reactions = available_reactions;
|
||||
|
||||
@@ -67,9 +67,7 @@ fn default_reaction_other_color() -> String {
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timezone: default_timezone(),
|
||||
}
|
||||
Self { timezone: default_timezone() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,15 +130,13 @@ impl Config {
|
||||
}
|
||||
|
||||
match fs::read_to_string(&config_path) {
|
||||
Ok(content) => {
|
||||
match toml::from_str::<Config>(&content) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not parse config file: {}", e);
|
||||
Self::default()
|
||||
}
|
||||
Ok(content) => match toml::from_str::<Config>(&content) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not parse config file: {}", e);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not read config file: {}", e);
|
||||
Self::default()
|
||||
@@ -150,8 +146,8 @@ impl Config {
|
||||
|
||||
/// Сохранить конфигурацию в файл
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
let config_dir = Self::config_dir()
|
||||
.ok_or_else(|| "Could not determine config directory".to_string())?;
|
||||
let config_dir =
|
||||
Self::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
|
||||
|
||||
// Создаём директорию если её нет
|
||||
fs::create_dir_all(&config_dir)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
use crossterm::event::KeyCode;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
|
||||
pub async fn handle(app: &mut App, key_code: KeyCode) {
|
||||
match &app.td_client.auth_state {
|
||||
@@ -18,7 +18,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
|
||||
KeyCode::Enter => {
|
||||
if !app.phone_input.is_empty() {
|
||||
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(_)) => {
|
||||
app.error_message = None;
|
||||
app.status_message = None;
|
||||
@@ -48,7 +53,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
|
||||
KeyCode::Enter => {
|
||||
if !app.code_input.is_empty() {
|
||||
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(_)) => {
|
||||
app.error_message = None;
|
||||
app.status_message = None;
|
||||
@@ -78,7 +88,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
|
||||
KeyCode::Enter => {
|
||||
if !app.password_input.is_empty() {
|
||||
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(_)) => {
|
||||
app.error_message = None;
|
||||
app.status_message = None;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::ChatAction;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::timeout;
|
||||
use crate::app::App;
|
||||
use crate::tdlib::ChatAction;
|
||||
|
||||
pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
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 let Some(chat_id) = app.get_selected_chat_id() {
|
||||
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)) => {
|
||||
if messages.is_empty() {
|
||||
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 => {
|
||||
// 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();
|
||||
}
|
||||
return;
|
||||
@@ -125,13 +133,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if profile.username.is_some() {
|
||||
if action_index == current_idx {
|
||||
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) {
|
||||
Ok(_) => {
|
||||
app.status_message = Some(format!("Открыто: {}", url));
|
||||
}
|
||||
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
|
||||
if action_index == current_idx {
|
||||
app.status_message = Some(format!("ID скопирован: {}", profile.chat_id));
|
||||
app.status_message =
|
||||
Some(format!("ID скопирован: {}", profile.chat_id));
|
||||
return;
|
||||
}
|
||||
current_idx += 1;
|
||||
@@ -174,10 +187,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
KeyCode::Enter => {
|
||||
// Перейти к выбранному сообщению
|
||||
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()
|
||||
.position(|m| m.id == msg_id);
|
||||
|
||||
|
||||
if let Some(idx) = msg_index {
|
||||
let total = app.td_client.current_chat_messages.len();
|
||||
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 let Ok(Ok(results)) = timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client.search_messages(chat_id, &app.message_search_query)
|
||||
).await {
|
||||
app.td_client
|
||||
.search_messages(chat_id, &app.message_search_query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
app.set_search_results(results);
|
||||
}
|
||||
} 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 Ok(Ok(results)) = timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client.search_messages(chat_id, &app.message_search_query)
|
||||
).await {
|
||||
app.td_client
|
||||
.search_messages(chat_id, &app.message_search_query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
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() {
|
||||
// Ищем индекс сообщения в текущей истории
|
||||
let msg_index = app.td_client.current_chat_messages
|
||||
let msg_index = app
|
||||
.td_client
|
||||
.current_chat_messages
|
||||
.iter()
|
||||
.position(|m| m.id == msg_id);
|
||||
|
||||
|
||||
if let Some(idx) = msg_index {
|
||||
// Вычисляем scroll offset чтобы показать сообщение
|
||||
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 {
|
||||
app.status_message = Some("Отправка реакции...".to_string());
|
||||
app.needs_redraw = true;
|
||||
|
||||
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.toggle_reaction(chat_id, message_id, emoji.clone())
|
||||
).await {
|
||||
app.td_client
|
||||
.toggle_reaction(chat_id, message_id, emoji.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
app.status_message = Some(format!("Реакция {} добавлена", emoji));
|
||||
app.status_message =
|
||||
Some(format!("Реакция {} добавлена", emoji));
|
||||
app.exit_reaction_picker_mode();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
@@ -300,7 +327,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
Err(_) => {
|
||||
app.error_message = Some("Таймаут отправки реакции".to_string());
|
||||
app.error_message =
|
||||
Some("Таймаут отправки реакции".to_string());
|
||||
app.status_message = None;
|
||||
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(chat_id) = app.get_selected_chat_id() {
|
||||
// Находим сообщение для проверки 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()
|
||||
.find(|m| m.id == msg_id)
|
||||
.map(|m| m.can_be_deleted_for_all_users)
|
||||
@@ -334,11 +364,19 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.delete_messages(chat_id, vec![msg_id], can_delete_for_all)
|
||||
).await {
|
||||
app.td_client.delete_messages(
|
||||
chat_id,
|
||||
vec![msg_id],
|
||||
can_delete_for_all,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
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;
|
||||
}
|
||||
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() {
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.forward_messages(to_chat_id, from_chat_id, vec![msg_id])
|
||||
).await {
|
||||
app.td_client.forward_messages(
|
||||
to_chat_id,
|
||||
from_chat_id,
|
||||
vec![msg_id],
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
app.status_message = Some("Сообщение переслано".to_string());
|
||||
app.status_message =
|
||||
Some("Сообщение переслано".to_string());
|
||||
}
|
||||
Ok(Err(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() {
|
||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||
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(_)) => {
|
||||
// Загружаем недостающие 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.status_message = None;
|
||||
@@ -460,8 +518,6 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Enter - открыть чат, отправить сообщение или редактировать
|
||||
if key.code == KeyCode::Enter {
|
||||
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.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)) => {
|
||||
// Обновляем сообщение в списке
|
||||
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.entities = edited_msg.entities;
|
||||
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;
|
||||
|
||||
// Отменяем 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)) => {
|
||||
// Добавляем отправленное сообщение в список (с лимитом)
|
||||
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() {
|
||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||
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(_)) => {
|
||||
// Загружаем недостающие 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.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;
|
||||
} 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();
|
||||
@@ -616,7 +706,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => {
|
||||
// Показать модалку подтверждения удаления
|
||||
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 {
|
||||
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() {
|
||||
let chat_id = app.selected_chat_id.unwrap();
|
||||
let message_id = msg.id;
|
||||
|
||||
|
||||
app.status_message = Some("Загрузка реакций...".to_string());
|
||||
app.needs_redraw = true;
|
||||
|
||||
|
||||
// Запрашиваем доступные реакции
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.get_message_available_reactions(chat_id, message_id)
|
||||
).await {
|
||||
app.td_client
|
||||
.get_message_available_reactions(chat_id, message_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(reactions)) => {
|
||||
if reactions.is_empty() {
|
||||
app.error_message = Some("Реакции недоступны для этого сообщения".to_string());
|
||||
app.error_message =
|
||||
Some("Реакции недоступны для этого сообщения".to_string());
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
} else {
|
||||
@@ -691,7 +786,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if key.code == KeyCode::Char('u') && has_ctrl {
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
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)) => {
|
||||
app.profile_info = Some(profile);
|
||||
app.enter_profile_mode();
|
||||
@@ -756,12 +852,15 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.cursor_position += 1;
|
||||
|
||||
// Отправляем 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)
|
||||
.unwrap_or(true);
|
||||
if should_send_typing {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -804,18 +903,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
|
||||
// Проверяем, нужно ли подгрузить старые сообщения
|
||||
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 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(
|
||||
Duration::from_secs(3),
|
||||
app.td_client.load_older_messages(chat_id, oldest_msg_id, 20)
|
||||
).await {
|
||||
app.td_client
|
||||
.load_older_messages(chat_id, oldest_msg_id, 20),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !older.is_empty() {
|
||||
// Добавляем старые сообщения в начало
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -848,7 +958,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.selected_folder_id = Some(folder_id);
|
||||
// Загружаем чаты папки
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -862,73 +976,76 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
/// Подсчёт количества доступных действий в профиле
|
||||
fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
||||
let mut count = 0;
|
||||
|
||||
|
||||
if profile.username.is_some() {
|
||||
count += 1; // Открыть в браузере
|
||||
}
|
||||
|
||||
|
||||
count += 1; // Скопировать ID
|
||||
|
||||
|
||||
if profile.is_group {
|
||||
count += 1; // Покинуть группу
|
||||
}
|
||||
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// Копирует текст в системный буфер обмена
|
||||
fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||
use arboard::Clipboard;
|
||||
|
||||
let mut clipboard = Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||||
clipboard.set_text(text).map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||||
|
||||
|
||||
let mut clipboard =
|
||||
Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||||
clipboard
|
||||
.set_text(text)
|
||||
.map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Форматирует сообщение для копирования с контекстом
|
||||
fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
|
||||
// Добавляем forward контекст если есть
|
||||
if let Some(forward) = &msg.forward_from {
|
||||
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||||
}
|
||||
|
||||
|
||||
// Добавляем reply контекст если есть
|
||||
if let Some(reply) = &msg.reply_to {
|
||||
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||||
}
|
||||
|
||||
|
||||
// Добавляем основной текст с markdown форматированием
|
||||
result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities));
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Конвертирует текст с entities в markdown
|
||||
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
|
||||
|
||||
if entities.is_empty() {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
|
||||
// Создаём вектор символов для работы с unicode
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut result = String::new();
|
||||
let mut i = 0;
|
||||
|
||||
|
||||
while i < chars.len() {
|
||||
// Ищем entity, который начинается в текущей позиции
|
||||
let mut entity_found = false;
|
||||
|
||||
|
||||
for entity in entities {
|
||||
if entity.offset as usize == i {
|
||||
entity_found = true;
|
||||
let end = (entity.offset + entity.length) as usize;
|
||||
let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
|
||||
|
||||
|
||||
// Применяем форматирование в зависимости от типа
|
||||
let formatted = match &entity.r#type {
|
||||
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),
|
||||
_ => entity_text,
|
||||
};
|
||||
|
||||
|
||||
result.push_str(&formatted);
|
||||
i = end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !entity_found {
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
39
src/main.rs
39
src/main.rs
@@ -46,11 +46,7 @@ async fn main() -> Result<(), io::Error> {
|
||||
|
||||
// Restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
@@ -91,20 +87,20 @@ async fn run_app<B: ratatui::backend::Backend>(
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = tdlib_rs::functions::set_tdlib_parameters(
|
||||
false, // use_test_dc
|
||||
"tdlib_data".to_string(), // database_directory
|
||||
"".to_string(), // files_directory
|
||||
"".to_string(), // database_encryption_key
|
||||
true, // use_file_database
|
||||
true, // use_chat_info_database
|
||||
true, // use_message_database
|
||||
false, // use_secret_chats
|
||||
false, // use_test_dc
|
||||
"tdlib_data".to_string(), // database_directory
|
||||
"".to_string(), // files_directory
|
||||
"".to_string(), // database_encryption_key
|
||||
true, // use_file_database
|
||||
true, // use_chat_info_database
|
||||
true, // use_message_database
|
||||
false, // use_secret_chats
|
||||
api_id,
|
||||
api_hash,
|
||||
"en".to_string(), // system_language_code
|
||||
"Desktop".to_string(), // device_model
|
||||
"".to_string(), // system_version
|
||||
env!("CARGO_PKG_VERSION").to_string(), // application_version
|
||||
"en".to_string(), // system_language_code
|
||||
"Desktop".to_string(), // device_model
|
||||
"".to_string(), // system_version
|
||||
env!("CARGO_PKG_VERSION").to_string(), // application_version
|
||||
client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -156,7 +152,9 @@ async fn run_app<B: ratatui::backend::Backend>(
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
// 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
|
||||
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;
|
||||
|
||||
// Ждём завершения polling задачи (с таймаутом)
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
polling_handle
|
||||
).await;
|
||||
let _ = tokio::time::timeout(Duration::from_secs(2), polling_handle).await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::env;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
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;
|
||||
|
||||
/// Максимальный размер кэшей пользователей
|
||||
@@ -311,7 +314,11 @@ impl TdClient {
|
||||
/// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to)
|
||||
pub fn push_message(&mut self, msg: MessageInfo) {
|
||||
// Проверяем, есть ли уже сообщение с таким 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, или старое не имеет — заменяем
|
||||
if msg.reply_to.is_some() || self.current_chat_messages[idx].reply_to.is_none() {
|
||||
self.current_chat_messages[idx] = msg;
|
||||
@@ -350,7 +357,8 @@ impl TdClient {
|
||||
/// Например: "Вася печатает..."
|
||||
pub fn get_typing_text(&self) -> Option<String> {
|
||||
self.typing_status.as_ref().map(|(user_id, action, _)| {
|
||||
let name = self.user_names
|
||||
let name = self
|
||||
.user_names
|
||||
.peek(user_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Кто-то".to_string());
|
||||
@@ -361,20 +369,20 @@ impl TdClient {
|
||||
/// Инициализация TDLib с параметрами
|
||||
pub async fn init(&mut self) -> Result<(), String> {
|
||||
let result = functions::set_tdlib_parameters(
|
||||
false, // use_test_dc
|
||||
"tdlib_data".to_string(), // database_directory
|
||||
"".to_string(), // files_directory
|
||||
"".to_string(), // database_encryption_key
|
||||
true, // use_file_database
|
||||
true, // use_chat_info_database
|
||||
true, // use_message_database
|
||||
false, // use_secret_chats
|
||||
self.api_id, // api_id
|
||||
self.api_hash.clone(), // api_hash
|
||||
"en".to_string(), // system_language_code
|
||||
"Desktop".to_string(), // device_model
|
||||
"".to_string(), // system_version
|
||||
env!("CARGO_PKG_VERSION").to_string(), // application_version
|
||||
false, // use_test_dc
|
||||
"tdlib_data".to_string(), // database_directory
|
||||
"".to_string(), // files_directory
|
||||
"".to_string(), // database_encryption_key
|
||||
true, // use_file_database
|
||||
true, // use_chat_info_database
|
||||
true, // use_message_database
|
||||
false, // use_secret_chats
|
||||
self.api_id, // api_id
|
||||
self.api_hash.clone(), // api_hash
|
||||
"en".to_string(), // system_language_code
|
||||
"Desktop".to_string(), // device_model
|
||||
"".to_string(), // system_version
|
||||
env!("CARGO_PKG_VERSION").to_string(), // application_version
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -457,7 +465,9 @@ impl TdClient {
|
||||
if update.position.order == 0 {
|
||||
// Чат больше не в Main (перемещён в архив и т.д.)
|
||||
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.is_pinned = update.position.is_pinned;
|
||||
@@ -493,7 +503,10 @@ impl TdClient {
|
||||
let is_incoming = !msg_info.is_outgoing;
|
||||
|
||||
// Проверяем, есть ли уже сообщение с таким 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 {
|
||||
Some(idx) => {
|
||||
@@ -505,8 +518,10 @@ impl TdClient {
|
||||
// но сохраняем reply_to (добавленный при отправке)
|
||||
let existing = &mut self.current_chat_messages[idx];
|
||||
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_for_all_users = msg_info.can_be_deleted_for_all_users;
|
||||
existing.can_be_deleted_only_for_self =
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -529,9 +544,8 @@ impl TdClient {
|
||||
if user.first_name.is_empty() && user.last_name.is_empty() {
|
||||
// Удаляем чаты с этим пользователем из списка
|
||||
let user_id = user.id;
|
||||
self.chats.retain(|c| {
|
||||
self.chat_user_ids.get(&c.id) != Some(&user_id)
|
||||
});
|
||||
self.chats
|
||||
.retain(|c| self.chat_user_ids.get(&c.id) != Some(&user_id));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,7 +564,8 @@ impl TdClient {
|
||||
// Обновляем username в чатах, связанных с этим пользователем
|
||||
for (&chat_id, &user_id) in &self.chat_user_ids.clone() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -564,10 +579,7 @@ impl TdClient {
|
||||
self.folders = update
|
||||
.chat_folders
|
||||
.into_iter()
|
||||
.map(|f| FolderInfo {
|
||||
id: f.id,
|
||||
name: f.title,
|
||||
})
|
||||
.map(|f| FolderInfo { id: f.id, name: f.title })
|
||||
.collect();
|
||||
self.main_chat_list_position = update.main_chat_list_position;
|
||||
}
|
||||
@@ -607,14 +619,26 @@ impl TdClient {
|
||||
let action_text = match update.action {
|
||||
ChatAction::Typing => Some("печатает...".to_string()),
|
||||
ChatAction::RecordingVideo => Some("записывает видео...".to_string()),
|
||||
ChatAction::UploadingVideo(_) => Some("отправляет видео...".to_string()),
|
||||
ChatAction::RecordingVoiceNote => Some("записывает голосовое...".to_string()),
|
||||
ChatAction::UploadingVoiceNote(_) => Some("отправляет голосовое...".to_string()),
|
||||
ChatAction::UploadingVideo(_) => {
|
||||
Some("отправляет видео...".to_string())
|
||||
}
|
||||
ChatAction::RecordingVoiceNote => {
|
||||
Some("записывает голосовое...".to_string())
|
||||
}
|
||||
ChatAction::UploadingVoiceNote(_) => {
|
||||
Some("отправляет голосовое...".to_string())
|
||||
}
|
||||
ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()),
|
||||
ChatAction::UploadingDocument(_) => Some("отправляет файл...".to_string()),
|
||||
ChatAction::UploadingDocument(_) => {
|
||||
Some("отправляет файл...".to_string())
|
||||
}
|
||||
ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()),
|
||||
ChatAction::RecordingVideoNote => Some("записывает видеосообщение...".to_string()),
|
||||
ChatAction::UploadingVideoNote(_) => Some("отправляет видеосообщение...".to_string()),
|
||||
ChatAction::RecordingVideoNote => {
|
||||
Some("записывает видеосообщение...".to_string())
|
||||
}
|
||||
ChatAction::UploadingVideoNote(_) => {
|
||||
Some("отправляет видеосообщение...".to_string())
|
||||
}
|
||||
ChatAction::Cancel => None, // Отмена — сбрасываем статус
|
||||
_ => None,
|
||||
};
|
||||
@@ -633,7 +657,9 @@ impl TdClient {
|
||||
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| {
|
||||
// Извлекаем текст из 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())
|
||||
} else {
|
||||
None
|
||||
@@ -644,7 +670,11 @@ impl TdClient {
|
||||
Update::MessageInteractionInfo(update) => {
|
||||
// Обновляем реакции в текущем открытом чате
|
||||
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
|
||||
msg.reactions = update
|
||||
.interaction_info
|
||||
@@ -656,8 +686,12 @@ impl TdClient {
|
||||
.iter()
|
||||
.filter_map(|reaction| {
|
||||
let emoji = match &reaction.r#type {
|
||||
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None,
|
||||
tdlib_rs::enums::ReactionType::Emoji(e) => {
|
||||
e.emoji.clone()
|
||||
}
|
||||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => {
|
||||
return None
|
||||
}
|
||||
};
|
||||
|
||||
Some(ReactionInfo {
|
||||
@@ -697,9 +731,10 @@ impl TdClient {
|
||||
}
|
||||
|
||||
// Ищем позицию в Main списке (если есть)
|
||||
let main_position = td_chat.positions.iter().find(|pos| {
|
||||
matches!(pos.list, ChatList::Main)
|
||||
});
|
||||
let main_position = td_chat
|
||||
.positions
|
||||
.iter()
|
||||
.find(|pos| matches!(pos.list, ChatList::Main));
|
||||
|
||||
// Получаем order и is_pinned из позиции, или используем значения по умолчанию
|
||||
let (order, is_pinned) = main_position
|
||||
@@ -716,7 +751,9 @@ impl TdClient {
|
||||
let username = match &td_chat.r#type {
|
||||
ChatType::Private(private) => {
|
||||
// Ограничиваем размер 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() {
|
||||
self.chat_user_ids.remove(&key);
|
||||
@@ -724,7 +761,9 @@ impl TdClient {
|
||||
}
|
||||
self.chat_user_ids.insert(td_chat.id, private.user_id);
|
||||
// Проверяем, есть ли уже 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,
|
||||
};
|
||||
@@ -784,7 +823,13 @@ impl TdClient {
|
||||
// Ограничиваем количество чатов
|
||||
if self.chats.len() > MAX_CHATS {
|
||||
// Удаляем чат с наименьшим 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);
|
||||
}
|
||||
}
|
||||
@@ -891,11 +936,7 @@ impl TdClient {
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
Some(ReplyInfo {
|
||||
message_id: reply.message_id,
|
||||
sender_name,
|
||||
text,
|
||||
})
|
||||
Some(ReplyInfo { message_id: reply.message_id, sender_name, text })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -905,10 +946,7 @@ impl TdClient {
|
||||
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
|
||||
message.forward_info.as_ref().map(|info| {
|
||||
let sender_name = self.get_origin_sender_name(&info.origin);
|
||||
ForwardInfo {
|
||||
sender_name,
|
||||
date: info.date,
|
||||
}
|
||||
ForwardInfo { sender_name, date: info.date }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -944,24 +982,24 @@ impl TdClient {
|
||||
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
|
||||
use tdlib_rs::enums::MessageOrigin;
|
||||
match origin {
|
||||
MessageOrigin::User(u) => {
|
||||
self.user_names.peek(&u.sender_user_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("User_{}", u.sender_user_id))
|
||||
}
|
||||
MessageOrigin::Chat(c) => {
|
||||
self.chats.iter()
|
||||
.find(|chat| chat.id == c.sender_chat_id)
|
||||
.map(|chat| chat.title.clone())
|
||||
.unwrap_or_else(|| "Чат".to_string())
|
||||
}
|
||||
MessageOrigin::User(u) => self
|
||||
.user_names
|
||||
.peek(&u.sender_user_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("User_{}", u.sender_user_id)),
|
||||
MessageOrigin::Chat(c) => self
|
||||
.chats
|
||||
.iter()
|
||||
.find(|chat| chat.id == c.sender_chat_id)
|
||||
.map(|chat| chat.title.clone())
|
||||
.unwrap_or_else(|| "Чат".to_string()),
|
||||
MessageOrigin::HiddenUser(h) => h.sender_name.clone(),
|
||||
MessageOrigin::Channel(c) => {
|
||||
self.chats.iter()
|
||||
.find(|chat| chat.id == c.chat_id)
|
||||
.map(|chat| chat.title.clone())
|
||||
.unwrap_or_else(|| "Канал".to_string())
|
||||
}
|
||||
MessageOrigin::Channel(c) => self
|
||||
.chats
|
||||
.iter()
|
||||
.find(|chat| chat.id == c.chat_id)
|
||||
.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
|
||||
{
|
||||
let sender_name = match &msg.sender_id {
|
||||
tdlib_rs::enums::MessageSender::User(user) => {
|
||||
self.user_names
|
||||
.get(&user.user_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("User_{}", user.user_id))
|
||||
}
|
||||
tdlib_rs::enums::MessageSender::Chat(chat) => {
|
||||
self.chats
|
||||
.iter()
|
||||
.find(|c| c.id == chat.chat_id)
|
||||
.map(|c| c.title.clone())
|
||||
.unwrap_or_else(|| "Чат".to_string())
|
||||
}
|
||||
tdlib_rs::enums::MessageSender::User(user) => self
|
||||
.user_names
|
||||
.get(&user.user_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("User_{}", user.user_id)),
|
||||
tdlib_rs::enums::MessageSender::Chat(chat) => self
|
||||
.chats
|
||||
.iter()
|
||||
.find(|c| c.id == chat.chat_id)
|
||||
.map(|c| c.title.clone())
|
||||
.unwrap_or_else(|| "Чат".to_string()),
|
||||
};
|
||||
let (content, _) = extract_message_text_static(&msg);
|
||||
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> {
|
||||
let result = functions::set_authentication_phone_number(
|
||||
phone,
|
||||
None,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
let result = functions::set_authentication_phone_number(phone, None, self.client_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -1103,12 +1134,7 @@ impl TdClient {
|
||||
|
||||
/// Загрузка списка чатов
|
||||
pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
|
||||
let result = functions::load_chats(
|
||||
Some(ChatList::Main),
|
||||
limit,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
let result = functions::load_chats(Some(ChatList::Main), limit, self.client_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -1118,16 +1144,10 @@ impl TdClient {
|
||||
|
||||
/// Загрузка чатов для конкретной папки
|
||||
pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
|
||||
let chat_list = ChatList::Folder(tdlib_rs::types::ChatListFolder {
|
||||
chat_folder_id: folder_id,
|
||||
});
|
||||
let chat_list =
|
||||
ChatList::Folder(tdlib_rs::types::ChatListFolder { chat_folder_id: folder_id });
|
||||
|
||||
let result = functions::load_chats(
|
||||
Some(chat_list),
|
||||
limit,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
let result = functions::load_chats(Some(chat_list), limit, self.client_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -1155,9 +1175,9 @@ impl TdClient {
|
||||
let result = functions::get_chat_history(
|
||||
chat_id,
|
||||
from_message_id,
|
||||
0, // offset
|
||||
0, // offset
|
||||
limit,
|
||||
false, // only_local - загружаем с сервера!
|
||||
false, // only_local - загружаем с сервера!
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -1209,8 +1229,8 @@ impl TdClient {
|
||||
let _ = functions::view_messages(
|
||||
chat_id,
|
||||
message_ids,
|
||||
None, // source
|
||||
true, // force_read
|
||||
None, // source
|
||||
true, // force_read
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -1223,14 +1243,14 @@ impl TdClient {
|
||||
pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> {
|
||||
let result = functions::search_chat_messages(
|
||||
chat_id,
|
||||
"".to_string(), // query
|
||||
None, // sender_id
|
||||
0, // from_message_id
|
||||
0, // offset
|
||||
100, // limit
|
||||
Some(SearchMessagesFilter::Pinned), // filter
|
||||
0, // message_thread_id
|
||||
0, // saved_messages_topic_id
|
||||
"".to_string(), // query
|
||||
None, // sender_id
|
||||
0, // from_message_id
|
||||
0, // offset
|
||||
100, // limit
|
||||
Some(SearchMessagesFilter::Pinned), // filter
|
||||
0, // message_thread_id
|
||||
0, // saved_messages_topic_id
|
||||
self.client_id,
|
||||
)
|
||||
.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() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -1287,13 +1311,13 @@ impl TdClient {
|
||||
let result = functions::search_chat_messages(
|
||||
chat_id,
|
||||
query.to_string(),
|
||||
None, // sender_id
|
||||
0, // from_message_id
|
||||
0, // offset
|
||||
50, // limit
|
||||
None, // filter (no filter = search by text)
|
||||
0, // message_thread_id
|
||||
0, // saved_messages_topic_id
|
||||
None, // sender_id
|
||||
0, // from_message_id
|
||||
0, // offset
|
||||
50, // limit
|
||||
None, // filter (no filter = search by text)
|
||||
0, // message_thread_id
|
||||
0, // saved_messages_topic_id
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -1359,8 +1383,12 @@ impl TdClient {
|
||||
profile.online_status = Some(match user.status {
|
||||
tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(),
|
||||
tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(),
|
||||
tdlib_rs::enums::UserStatus::LastWeek(_) => "Был(а) на этой неделе".to_string(),
|
||||
tdlib_rs::enums::UserStatus::LastMonth(_) => "Был(а) в этом месяце".to_string(),
|
||||
tdlib_rs::enums::UserStatus::LastWeek(_) => {
|
||||
"Был(а) на этой неделе".to_string()
|
||||
}
|
||||
tdlib_rs::enums::UserStatus::LastMonth(_) => {
|
||||
"Был(а) в этом месяце".to_string()
|
||||
}
|
||||
tdlib_rs::enums::UserStatus::Offline(offline) => {
|
||||
crate::utils::format_was_online(offline.was_online)
|
||||
}
|
||||
@@ -1369,8 +1397,10 @@ impl TdClient {
|
||||
}
|
||||
|
||||
// Bio (getUserFullInfo)
|
||||
let 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 {
|
||||
let 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 {
|
||||
profile.bio = Some(bio_obj.text);
|
||||
}
|
||||
@@ -1381,14 +1411,21 @@ impl TdClient {
|
||||
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 {
|
||||
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;
|
||||
if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = full_info_result {
|
||||
let full_info_result = functions::get_basic_group_full_info(
|
||||
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() {
|
||||
profile.description = Some(full_info.description);
|
||||
}
|
||||
@@ -1399,9 +1436,14 @@ impl TdClient {
|
||||
}
|
||||
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 {
|
||||
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.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;
|
||||
if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = full_info_result {
|
||||
let 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() {
|
||||
profile.description = Some(full_info.description);
|
||||
}
|
||||
@@ -1451,9 +1497,9 @@ impl TdClient {
|
||||
let result = functions::get_chat_history(
|
||||
chat_id,
|
||||
from_message_id,
|
||||
0, // offset
|
||||
0, // offset
|
||||
limit,
|
||||
false, // only_local
|
||||
false, // only_local
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -1497,11 +1543,9 @@ impl TdClient {
|
||||
/// Получение моего user_id
|
||||
pub async fn get_me(&self) -> Result<i64, String> {
|
||||
match functions::get_me(self.client_id).await {
|
||||
Ok(user) => {
|
||||
match user {
|
||||
User::User(u) => Ok(u.id),
|
||||
}
|
||||
}
|
||||
Ok(user) => match user {
|
||||
User::User(u) => Ok(u.id),
|
||||
},
|
||||
Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)),
|
||||
}
|
||||
}
|
||||
@@ -1513,30 +1557,37 @@ impl TdClient {
|
||||
0, // message_thread_id
|
||||
Some(action),
|
||||
self.client_id,
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Отправка текстового сообщения с поддержкой 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> {
|
||||
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage};
|
||||
use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo};
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
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 в тексте
|
||||
let formatted_text = match functions::parse_text_entities(
|
||||
text.clone(),
|
||||
TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }),
|
||||
self.client_id,
|
||||
).await {
|
||||
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText {
|
||||
text: ft.text,
|
||||
entities: ft.entities,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => {
|
||||
FormattedText { text: ft.text, entities: ft.entities }
|
||||
}
|
||||
Err(_) => {
|
||||
// Если парсинг не удался, отправляем как plain text
|
||||
FormattedText {
|
||||
text: text.clone(),
|
||||
entities: vec![],
|
||||
}
|
||||
FormattedText { text: text.clone(), entities: vec![] }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1558,9 +1609,9 @@ impl TdClient {
|
||||
|
||||
let result = functions::send_message(
|
||||
chat_id,
|
||||
0, // message_thread_id
|
||||
0, // message_thread_id
|
||||
reply_to,
|
||||
None, // options
|
||||
None, // options
|
||||
content,
|
||||
self.client_id,
|
||||
)
|
||||
@@ -1592,7 +1643,6 @@ impl TdClient {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Получить доступные реакции для сообщения
|
||||
pub async fn get_message_available_reactions(
|
||||
&mut self,
|
||||
@@ -1653,9 +1703,9 @@ impl TdClient {
|
||||
message_id: i64,
|
||||
emoji: String,
|
||||
) -> Result<(), String> {
|
||||
use tdlib_rs::enums::ReactionType;
|
||||
use tdlib_rs::functions;
|
||||
use tdlib_rs::types::ReactionTypeEmoji;
|
||||
use tdlib_rs::enums::ReactionType;
|
||||
|
||||
let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji });
|
||||
|
||||
@@ -1678,17 +1728,18 @@ impl TdClient {
|
||||
/// Редактирование текстового сообщения с поддержкой Markdown
|
||||
/// Устанавливает черновик для чата через TDLib API
|
||||
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::types::{DraftMessage, FormattedText, InputMessageText};
|
||||
|
||||
if text.is_empty() {
|
||||
// Очищаем черновик
|
||||
let result = functions::set_chat_draft_message(
|
||||
chat_id,
|
||||
0, // message_thread_id
|
||||
0, // message_thread_id
|
||||
None, // draft_message (None = очистить)
|
||||
self.client_id,
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -1696,10 +1747,7 @@ impl TdClient {
|
||||
}
|
||||
} else {
|
||||
// Создаём черновик
|
||||
let formatted_text = FormattedText {
|
||||
text: text.clone(),
|
||||
entities: vec![],
|
||||
};
|
||||
let formatted_text = FormattedText { text: text.clone(), entities: vec![] };
|
||||
|
||||
let input_message = InputMessageContent::InputMessageText(InputMessageText {
|
||||
text: formatted_text,
|
||||
@@ -1718,7 +1766,8 @@ impl TdClient {
|
||||
0, // message_thread_id
|
||||
Some(draft),
|
||||
self.client_id,
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
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> {
|
||||
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown};
|
||||
pub async fn edit_message(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
text: String,
|
||||
) -> Result<MessageInfo, String> {
|
||||
use tdlib_rs::enums::{InputMessageContent, TextParseMode};
|
||||
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown};
|
||||
|
||||
// Парсим markdown в тексте
|
||||
let formatted_text = match functions::parse_text_entities(
|
||||
text.clone(),
|
||||
TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }),
|
||||
self.client_id,
|
||||
).await {
|
||||
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText {
|
||||
text: ft.text,
|
||||
entities: ft.entities,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => {
|
||||
FormattedText { text: ft.text, entities: ft.entities }
|
||||
}
|
||||
Err(_) => {
|
||||
// Если парсинг не удался, отправляем как plain text
|
||||
FormattedText {
|
||||
text: text.clone(),
|
||||
entities: vec![],
|
||||
}
|
||||
FormattedText { text: text.clone(), entities: vec![] }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1756,13 +1808,8 @@ impl TdClient {
|
||||
clear_draft: true,
|
||||
});
|
||||
|
||||
let result = functions::edit_message_text(
|
||||
chat_id,
|
||||
message_id,
|
||||
content,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
functions::edit_message_text(chat_id, message_id, content, self.client_id).await;
|
||||
|
||||
match result {
|
||||
Ok(tdlib_rs::enums::Message::Message(msg)) => {
|
||||
@@ -1790,14 +1837,13 @@ impl TdClient {
|
||||
|
||||
/// Удаление сообщений
|
||||
/// revoke = true удаляет для всех, false только для себя
|
||||
pub async fn delete_messages(&self, chat_id: i64, message_ids: Vec<i64>, revoke: bool) -> Result<(), String> {
|
||||
let result = functions::delete_messages(
|
||||
chat_id,
|
||||
message_ids,
|
||||
revoke,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
pub async fn delete_messages(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
message_ids: Vec<i64>,
|
||||
revoke: bool,
|
||||
) -> Result<(), String> {
|
||||
let result = functions::delete_messages(chat_id, message_ids, revoke, self.client_id).await;
|
||||
|
||||
match result {
|
||||
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(
|
||||
to_chat_id,
|
||||
0, // message_thread_id
|
||||
from_chat_id,
|
||||
message_ids,
|
||||
None, // options
|
||||
None, // options
|
||||
false, // send_copy
|
||||
false, // remove_caption
|
||||
self.client_id,
|
||||
@@ -1832,8 +1883,8 @@ impl TdClient {
|
||||
let _ = functions::view_messages(
|
||||
chat_id,
|
||||
message_ids,
|
||||
None, // source
|
||||
true, // force_read
|
||||
None, // source
|
||||
true, // force_read
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
@@ -1847,7 +1898,8 @@ impl TdClient {
|
||||
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();
|
||||
|
||||
// Берём последние BATCH_SIZE элементов
|
||||
@@ -1885,16 +1937,17 @@ impl TdClient {
|
||||
/// Статическая функция для извлечения текста и entities сообщения (без &self)
|
||||
fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>) {
|
||||
match &message.content {
|
||||
MessageContent::MessageText(text) => {
|
||||
(text.text.text.clone(), text.text.entities.clone())
|
||||
}
|
||||
MessageContent::MessageText(text) => (text.text.text.clone(), text.text.entities.clone()),
|
||||
MessageContent::MessagePhoto(photo) => {
|
||||
if photo.caption.text.is_empty() {
|
||||
("[Фото]".to_string(), vec![])
|
||||
} else {
|
||||
// Добавляем смещение для "[Фото] " к entities
|
||||
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 {
|
||||
offset: e.offset + prefix_len,
|
||||
length: e.length,
|
||||
@@ -1909,7 +1962,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
|
||||
("[Видео]".to_string(), vec![])
|
||||
} else {
|
||||
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 {
|
||||
offset: e.offset + prefix_len,
|
||||
length: e.length,
|
||||
@@ -1932,7 +1988,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
|
||||
("[GIF]".to_string(), vec![])
|
||||
} else {
|
||||
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 {
|
||||
offset: e.offset + prefix_len,
|
||||
length: e.length,
|
||||
@@ -1942,9 +2001,7 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
|
||||
(format!("[GIF] {}", anim.caption.text), adjusted_entities)
|
||||
}
|
||||
}
|
||||
MessageContent::MessageAudio(audio) => {
|
||||
(format!("[Аудио: {}]", audio.audio.title), vec![])
|
||||
}
|
||||
MessageContent::MessageAudio(audio) => (format!("[Аудио: {}]", audio.audio.title), vec![]),
|
||||
MessageContent::MessageCall(_) => ("[Звонок]".to_string(), vec![]),
|
||||
MessageContent::MessagePoll(poll) => {
|
||||
(format!("[Опрос: {}]", poll.poll.question.text), vec![])
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
pub mod client;
|
||||
|
||||
pub use client::TdClient;
|
||||
pub use client::UserOnlineStatus;
|
||||
pub use client::ChatInfo;
|
||||
pub use client::FolderInfo;
|
||||
pub use client::ForwardInfo;
|
||||
pub use client::MessageInfo;
|
||||
pub use client::NetworkState;
|
||||
pub use client::ProfileInfo;
|
||||
pub use client::ChatInfo;
|
||||
pub use client::MessageInfo;
|
||||
pub use client::ReactionInfo;
|
||||
pub use client::ReplyInfo;
|
||||
pub use client::ForwardInfo;
|
||||
pub use client::FolderInfo;
|
||||
pub use client::TdClient;
|
||||
pub use client::UserOnlineStatus;
|
||||
pub use tdlib_rs::enums::ChatAction;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::UserOnlineStatus;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::UserOnlineStatus;
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
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 username_text = chat.username.as_ref()
|
||||
let username_text = chat
|
||||
.username
|
||||
.as_ref()
|
||||
.map(|u| format!(" {}", u))
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -78,7 +80,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
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) {
|
||||
@@ -100,13 +113,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
Block::default().borders(Borders::ALL)
|
||||
};
|
||||
|
||||
let chats_list = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.fg(Color::Yellow),
|
||||
);
|
||||
let chats_list = List::new(items).block(block).highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.fg(Color::Yellow),
|
||||
);
|
||||
|
||||
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);
|
||||
(formatted, Color::Gray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastWeek) => {
|
||||
("был(а) на этой неделе".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastMonth) => {
|
||||
("был(а) в этом месяце".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".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) {
|
||||
match app.td_client.get_user_status_by_chat_id(chat.id) {
|
||||
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)) => {
|
||||
let formatted = format_was_online(*was_online);
|
||||
(formatted, Color::Gray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastWeek) => {
|
||||
("был(а) на этой неделе".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastMonth) => {
|
||||
("был(а) в этом месяце".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LongTimeAgo) => {
|
||||
("был(а) давно".to_string(), Color::DarkGray)
|
||||
}
|
||||
None => ("".to_string(), Color::DarkGray),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::NetworkState;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::NetworkState;
|
||||
|
||||
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() {
|
||||
format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
} 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) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
@@ -18,10 +18,7 @@ pub fn render(f: &mut Frame, app: &App) {
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let message = app
|
||||
.status_message
|
||||
.as_deref()
|
||||
.unwrap_or("Загрузка...");
|
||||
let message = app.status_message.as_deref().unwrap_or("Загрузка...");
|
||||
|
||||
let loading = Paragraph::new(message)
|
||||
.style(
|
||||
@@ -30,11 +27,7 @@ pub fn render(f: &mut Frame, app: &App) {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" TTUI "),
|
||||
);
|
||||
.block(Block::default().borders(Borders::ALL).title(" TTUI "));
|
||||
|
||||
f.render_widget(loading, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use super::{chat_list, footer, messages};
|
||||
use crate::app::App;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use super::{chat_list, messages, footer};
|
||||
|
||||
/// Порог ширины для компактного режима (одна панель)
|
||||
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_widget = Paragraph::new(folders_line).block(
|
||||
Block::default()
|
||||
.title(" TTUI ")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
let folders_widget =
|
||||
Paragraph::new(folders_line).block(Block::default().title(" TTUI ").borders(Borders::ALL));
|
||||
|
||||
f.render_widget(folders_widget, area);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_date, format_timestamp_with_tz, get_day};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_timestamp_with_tz, format_date, get_day};
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
use tdlib_rs::types::TextEntity;
|
||||
|
||||
@@ -58,9 +58,16 @@ impl CharStyle {
|
||||
}
|
||||
|
||||
/// Преобразует текст с 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() {
|
||||
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
|
||||
}
|
||||
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::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 mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
|
||||
|
||||
@@ -186,10 +202,7 @@ struct WrappedLine {
|
||||
/// Возвращает строки с информацией о позициях для корректного применения entities
|
||||
fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
if max_width == 0 {
|
||||
return vec![WrappedLine {
|
||||
text: text.to_string(),
|
||||
start_offset: 0,
|
||||
}];
|
||||
return vec![WrappedLine { text: text.to_string(), start_offset: 0 }];
|
||||
}
|
||||
|
||||
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() {
|
||||
result.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: 0,
|
||||
});
|
||||
result.push(WrappedLine { text: String::new(), start_offset: 0 });
|
||||
}
|
||||
|
||||
result
|
||||
@@ -368,24 +378,28 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
// Показываем typing status: "👤 Имя @username печатает..."
|
||||
let mut spans = vec![
|
||||
Span::styled(
|
||||
format!("👤 {}", chat.title),
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let mut spans = vec![Span::styled(
|
||||
format!("👤 {}", chat.title),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)];
|
||||
if let Some(username) = &chat.username {
|
||||
spans.push(Span::styled(
|
||||
format!(" {}", username),
|
||||
Style::default().fg(Color::Gray),
|
||||
));
|
||||
spans
|
||||
.push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray)));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
format!(" {}", action),
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
));
|
||||
Line::from(spans)
|
||||
} else {
|
||||
@@ -396,17 +410,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
Line::from(Span::styled(
|
||||
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)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
let header = Paragraph::new(header_line).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(header, message_chunks[0]);
|
||||
|
||||
// Pinned bar (если есть закреплённое сообщение)
|
||||
if let Some(pinned_msg) = &app.td_client.current_pinned_message {
|
||||
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_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis);
|
||||
let pinned_hint = "Ctrl+P";
|
||||
@@ -421,8 +440,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
Span::raw(" ".repeat(padding)),
|
||||
Span::styled(pinned_hint, Style::default().fg(Color::Gray)),
|
||||
]);
|
||||
let pinned_bar = Paragraph::new(pinned_line)
|
||||
.style(Style::default().bg(Color::Rgb(40, 20, 40)));
|
||||
let pinned_bar =
|
||||
Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
|
||||
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 {
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
if msg.is_outgoing {
|
||||
@@ -540,16 +563,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
} else {
|
||||
// Forward слева для входящих
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(forward_line, Style::default().fg(Color::Magenta)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
forward_line,
|
||||
Style::default().fg(Color::Magenta),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
// Отображаем reply если есть
|
||||
if let Some(reply) = &msg.reply_to {
|
||||
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_len = reply_line.chars().count();
|
||||
|
||||
@@ -562,9 +590,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
} else {
|
||||
// Reply слева для входящих
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
reply_line,
|
||||
Style::default().fg(Color::Cyan),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,11 +622,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
);
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans = format_text_with_entities(
|
||||
&wrapped.text,
|
||||
&line_entities,
|
||||
msg_color,
|
||||
);
|
||||
let formatted_spans =
|
||||
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if is_last_line {
|
||||
// Последняя строка — добавляем 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 mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
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.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));
|
||||
} else {
|
||||
// Промежуточные строки — просто текст справа
|
||||
let padding = content_width.saturating_sub(line_len + marker_len + 1);
|
||||
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
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);
|
||||
lines.push(Line::from(line_spans));
|
||||
@@ -643,19 +682,24 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
);
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans = format_text_with_entities(
|
||||
&wrapped.text,
|
||||
&line_entities,
|
||||
msg_color,
|
||||
);
|
||||
let formatted_spans =
|
||||
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if i == 0 {
|
||||
// Первая строка — с временем и маркером выбора
|
||||
let mut line_spans = vec![];
|
||||
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.extend(formatted_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 {
|
||||
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 {
|
||||
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));
|
||||
@@ -723,10 +769,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Нет сообщений",
|
||||
Style::default().fg(Color::Gray),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray))));
|
||||
}
|
||||
|
||||
// Вычисляем скролл с учётом пользовательского offset
|
||||
@@ -769,10 +812,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Input box с wrap для длинного текста и блочным курсором
|
||||
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| {
|
||||
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)
|
||||
})
|
||||
.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() {
|
||||
// Режим выбора сообщения - подсказка зависит от возможностей
|
||||
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_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
|
||||
let can_edit = selected_msg
|
||||
.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) {
|
||||
(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, 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() {
|
||||
// Режим редактирования
|
||||
if app.message_input.is_empty() {
|
||||
@@ -804,16 +859,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
(line, " Редактирование (Esc отмена) ")
|
||||
} 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 отмена) ")
|
||||
}
|
||||
} else if app.is_replying() {
|
||||
// Режим ответа на сообщение
|
||||
let reply_preview = app.get_replying_to_message()
|
||||
let reply_preview = app
|
||||
.get_replying_to_message()
|
||||
.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 ellipsis = if m.content.chars().count() > 30 { "..." } else { "" };
|
||||
let ellipsis = if m.content.chars().count() > 30 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{}: {}{}", sender, text_preview, ellipsis)
|
||||
})
|
||||
.unwrap_or_else(|| "...".to_string());
|
||||
@@ -829,7 +898,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
} else {
|
||||
let short_preview: String = reply_preview.chars().take(15).collect();
|
||||
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 отмена) ")
|
||||
}
|
||||
} else {
|
||||
@@ -844,7 +918,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
(line, "")
|
||||
} 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, "")
|
||||
}
|
||||
};
|
||||
@@ -860,7 +939,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.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)
|
||||
@@ -882,7 +965,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Модалка выбора реакции
|
||||
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
|
||||
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() {
|
||||
Line::from(vec![
|
||||
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)),
|
||||
])
|
||||
};
|
||||
|
||||
let search_input = Paragraph::new(input_line)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Поиск по сообщениям ")
|
||||
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
);
|
||||
|
||||
let search_input = Paragraph::new(input_line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Поиск по сообщениям ")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
);
|
||||
f.render_widget(search_input, chunks[0]);
|
||||
|
||||
// Search results
|
||||
@@ -948,14 +1043,29 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Маркер выбора, имя и дата
|
||||
let marker = if is_selected { "▶ " } else { " " };
|
||||
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
|
||||
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
|
||||
let sender_color = if msg.is_outgoing {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
let sender_name = if msg.is_outgoing {
|
||||
"Вы".to_string()
|
||||
} else {
|
||||
msg.sender_name.clone()
|
||||
};
|
||||
|
||||
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(
|
||||
format!("{} ", sender_name),
|
||||
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(sender_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
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 wrapped = wrap_text_with_offsets(&msg.content, max_width);
|
||||
let wrapped_count = wrapped.len();
|
||||
@@ -998,20 +1112,35 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.border_style(Style::default().fg(Color::Yellow)),
|
||||
)
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(results_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
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::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::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)),
|
||||
@@ -1021,7 +1150,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.border_style(Style::default().fg(Color::Yellow)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
@@ -1046,9 +1175,13 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.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]);
|
||||
|
||||
// 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() {
|
||||
let is_selected = idx == app.selected_pinned_index;
|
||||
|
||||
|
||||
// Пустая строка между сообщениями
|
||||
if idx > 0 {
|
||||
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 sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
|
||||
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
|
||||
|
||||
let sender_color = if msg.is_outgoing {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
let sender_name = if msg.is_outgoing {
|
||||
"Вы".to_string()
|
||||
} else {
|
||||
msg.sender_name.clone()
|
||||
};
|
||||
|
||||
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(
|
||||
format!("{} ", sender_name),
|
||||
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(sender_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
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 wrapped = wrap_text_with_offsets(&msg.content, max_width);
|
||||
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![
|
||||
Span::raw(" "), // Отступ
|
||||
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::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(messages_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
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::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)),
|
||||
@@ -1141,7 +1304,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
@@ -1169,11 +1332,18 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
Line::from(""),
|
||||
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(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)),
|
||||
@@ -1194,9 +1364,13 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
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;
|
||||
|
||||
// Размеры модалки (зависят от количества реакций)
|
||||
@@ -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(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::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::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Отмена"),
|
||||
@@ -1262,7 +1446,11 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.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);
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
mod loading;
|
||||
mod auth;
|
||||
mod main_screen;
|
||||
pub mod chat_list;
|
||||
pub mod messages;
|
||||
pub mod footer;
|
||||
mod loading;
|
||||
mod main_screen;
|
||||
pub mod messages;
|
||||
pub mod profile;
|
||||
|
||||
use ratatui::Frame;
|
||||
use crate::app::{App, AppScreen};
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use crate::app::{App, AppScreen};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Минимальная высота терминала
|
||||
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) {
|
||||
let message = format!(
|
||||
"{}x{}\nМинимум: {}x{}",
|
||||
width, height, MIN_WIDTH, MIN_HEIGHT
|
||||
);
|
||||
let message = format!("{}x{}\nМинимум: {}x{}", width, height, MIN_WIDTH, MIN_HEIGHT);
|
||||
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);
|
||||
f.render_widget(warning, f.area());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::ProfileInfo;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Profile info
|
||||
Constraint::Length(3), // Actions help
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Profile info
|
||||
Constraint::Length(3), // Actions help
|
||||
])
|
||||
.split(area);
|
||||
|
||||
@@ -32,9 +32,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.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]);
|
||||
|
||||
// Profile info
|
||||
@@ -83,9 +87,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
|
||||
// Bio (только для личных чатов)
|
||||
if let Some(bio) = &profile.bio {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("О себе: ", Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled("О себе: ", Style::default().fg(Color::Gray))]));
|
||||
// Разбиваем bio на строки если длинное
|
||||
let bio_lines: Vec<&str> = bio.lines().collect();
|
||||
for bio_line in bio_lines {
|
||||
@@ -105,9 +107,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
|
||||
// Description (для групп/каналов)
|
||||
if let Some(desc) = &profile.description {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Описание: ", Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled("Описание: ", Style::default().fg(Color::Gray))]));
|
||||
let desc_lines: Vec<&str> = desc.lines().collect();
|
||||
for desc_line in desc_lines {
|
||||
lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White))));
|
||||
@@ -119,7 +119,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
if let Some(link) = &profile.invite_link {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Ссылка: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(link, Style::default().fg(Color::Blue).add_modifier(Modifier::UNDERLINED)),
|
||||
Span::styled(
|
||||
link,
|
||||
Style::default()
|
||||
.fg(Color::Blue)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
),
|
||||
]));
|
||||
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(
|
||||
"Действия:",
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
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 marker = if is_selected { "▶ " } else { " " };
|
||||
let style = if is_selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
@@ -154,17 +163,27 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
.scroll((0, 0));
|
||||
f.render_widget(info_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
let help_line = Line::from(vec![
|
||||
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" ↑↓ ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("навигация"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::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)),
|
||||
@@ -174,7 +193,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
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> {
|
||||
let mut actions = vec![];
|
||||
|
||||
|
||||
if profile.username.is_some() {
|
||||
actions.push("Открыть в браузере");
|
||||
}
|
||||
|
||||
|
||||
actions.push("Скопировать ID");
|
||||
|
||||
|
||||
if profile.is_group {
|
||||
actions.push("Покинуть группу");
|
||||
}
|
||||
|
||||
|
||||
actions
|
||||
}
|
||||
|
||||
@@ -212,12 +231,19 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
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(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::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" — нет"),
|
||||
@@ -230,7 +256,7 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Red))
|
||||
.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);
|
||||
|
||||
|
||||
10
src/utils.rs
10
src/utils.rs
@@ -105,21 +105,21 @@ pub fn get_day(timestamp: i32) -> i64 {
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
|
||||
pub fn format_datetime(timestamp: i32) -> String {
|
||||
let secs = timestamp as i64;
|
||||
|
||||
|
||||
// Время
|
||||
let hours = ((secs % 86400) / 3600) as u32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
let local_hours = (hours + 3) % 24; // MSK
|
||||
|
||||
|
||||
// Дата
|
||||
let days_since_epoch = secs / 86400;
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 1;
|
||||
let mut day = day_of_year as i32;
|
||||
|
||||
|
||||
for (i, &m) in months.iter().enumerate() {
|
||||
if day < m {
|
||||
month = i + 1;
|
||||
@@ -127,7 +127,7 @@ pub fn format_datetime(timestamp: i32) -> String {
|
||||
}
|
||||
day -= m;
|
||||
}
|
||||
|
||||
|
||||
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user