fixes
This commit is contained in:
11
CONTEXT.md
11
CONTEXT.md
@@ -32,6 +32,9 @@
|
|||||||
- **Отправка текстовых сообщений**
|
- **Отправка текстовых сообщений**
|
||||||
- **Редактирование сообщений**: ↑ при пустом инпуте → выбор → Enter → редактирование
|
- **Редактирование сообщений**: ↑ при пустом инпуте → выбор → Enter → редактирование
|
||||||
- **Удаление сообщений**: в режиме выбора нажать `d` / `в` / `Delete` → модалка подтверждения
|
- **Удаление сообщений**: в режиме выбора нажать `d` / `в` / `Delete` → модалка подтверждения
|
||||||
|
- **Reply на сообщения**: в режиме выбора нажать `r` / `к` → режим ответа с превью
|
||||||
|
- **Forward сообщений**: в режиме выбора нажать `f` / `а` → выбор чата для пересылки
|
||||||
|
- **Отображение пересланных сообщений**: индикатор "↪ Переслано от" с именем отправителя
|
||||||
- **Индикатор редактирования**: ✎ рядом с временем для отредактированных сообщений
|
- **Индикатор редактирования**: ✎ рядом с временем для отредактированных сообщений
|
||||||
- **Новые сообщения в реальном времени** при открытом чате
|
- **Новые сообщения в реальном времени** при открытом чате
|
||||||
- **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username
|
- **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username
|
||||||
@@ -78,10 +81,12 @@
|
|||||||
- `↑/↓` в открытом чате — скролл сообщений (с подгрузкой старых)
|
- `↑/↓` в открытом чате — скролл сообщений (с подгрузкой старых)
|
||||||
- `↑` при пустом инпуте — выбор сообщения для редактирования
|
- `↑` при пустом инпуте — выбор сообщения для редактирования
|
||||||
- `Enter` в режиме выбора — начать редактирование
|
- `Enter` в режиме выбора — начать редактирование
|
||||||
|
- `r` / `к` в режиме выбора — ответить на сообщение (reply)
|
||||||
|
- `f` / `а` в режиме выбора — переслать сообщение (forward)
|
||||||
- `d` / `в` / `Delete` в режиме выбора — удалить сообщение (с подтверждением)
|
- `d` / `в` / `Delete` в режиме выбора — удалить сообщение (с подтверждением)
|
||||||
- `y` / `н` / `Enter` — подтвердить удаление в модалке
|
- `y` / `н` / `Enter` — подтвердить удаление в модалке
|
||||||
- `n` / `т` / `Esc` — отменить удаление в модалке
|
- `n` / `т` / `Esc` — отменить удаление в модалке
|
||||||
- `Esc` — отменить выбор/редактирование
|
- `Esc` — отменить выбор/редактирование/reply
|
||||||
- `1-9` — переключение папок (в списке чатов)
|
- `1-9` — переключение папок (в списке чатов)
|
||||||
- **Редактирование текста в инпуте:**
|
- **Редактирование текста в инпуте:**
|
||||||
- `←` / `→` — перемещение курсора
|
- `←` / `→` — перемещение курсора
|
||||||
@@ -166,8 +171,8 @@ API_HASH=your_api_hash
|
|||||||
- [x] Markdown форматирование в сообщениях
|
- [x] Markdown форматирование в сообщениях
|
||||||
- [x] Редактирование сообщений
|
- [x] Редактирование сообщений
|
||||||
- [x] Удаление сообщений
|
- [x] Удаление сообщений
|
||||||
- [ ] Reply на сообщения
|
- [x] Reply на сообщения
|
||||||
- [ ] Forward сообщений
|
- [x] Forward сообщений
|
||||||
|
|
||||||
## Известные проблемы
|
## Известные проблемы
|
||||||
|
|
||||||
|
|||||||
12
ROADMAP.md
12
ROADMAP.md
@@ -101,5 +101,13 @@
|
|||||||
- Vim-style курсор █
|
- Vim-style курсор █
|
||||||
- Перемещение ←/→, Home/End
|
- Перемещение ←/→, Home/End
|
||||||
- Редактирование в любой позиции
|
- Редактирование в любой позиции
|
||||||
- [ ] Reply на сообщения
|
- [x] Reply на сообщения
|
||||||
- [ ] Forward сообщений
|
- `r` / `к` в режиме выбора → режим ответа
|
||||||
|
- Превью сообщения в поле ввода
|
||||||
|
- Esc для отмены
|
||||||
|
- [x] Forward сообщений
|
||||||
|
- `f` / `а` в режиме выбора → режим пересылки
|
||||||
|
- Превью сообщения в поле ввода
|
||||||
|
- Выбор чата стрелками, Enter для пересылки
|
||||||
|
- Esc для отмены
|
||||||
|
- Отображение "↪ Переслано от" для пересланных сообщений
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ pub struct App {
|
|||||||
// Delete confirmation
|
// Delete confirmation
|
||||||
/// ID сообщения для подтверждения удаления (показывает модалку)
|
/// ID сообщения для подтверждения удаления (показывает модалку)
|
||||||
pub confirm_delete_message_id: Option<i64>,
|
pub confirm_delete_message_id: Option<i64>,
|
||||||
|
// Reply state
|
||||||
|
/// ID сообщения, на которое отвечаем (None = обычная отправка)
|
||||||
|
pub replying_to_message_id: Option<i64>,
|
||||||
|
// Forward state
|
||||||
|
/// ID сообщения для пересылки
|
||||||
|
pub forwarding_message_id: Option<i64>,
|
||||||
|
/// Режим выбора чата для пересылки
|
||||||
|
pub is_selecting_forward_chat: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -68,6 +76,9 @@ impl App {
|
|||||||
editing_message_id: None,
|
editing_message_id: None,
|
||||||
selected_message_index: None,
|
selected_message_index: None,
|
||||||
confirm_delete_message_id: None,
|
confirm_delete_message_id: None,
|
||||||
|
replying_to_message_id: None,
|
||||||
|
forwarding_message_id: None,
|
||||||
|
is_selecting_forward_chat: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +134,7 @@ impl App {
|
|||||||
self.message_scroll_offset = 0;
|
self.message_scroll_offset = 0;
|
||||||
self.editing_message_id = None;
|
self.editing_message_id = None;
|
||||||
self.selected_message_index = None;
|
self.selected_message_index = None;
|
||||||
|
self.replying_to_message_id = None;
|
||||||
// Очищаем данные в TdClient
|
// Очищаем данные в TdClient
|
||||||
self.td_client.current_chat_id = None;
|
self.td_client.current_chat_id = None;
|
||||||
self.td_client.current_chat_messages.clear();
|
self.td_client.current_chat_messages.clear();
|
||||||
@@ -306,4 +318,62 @@ impl App {
|
|||||||
pub fn is_confirm_delete_shown(&self) -> bool {
|
pub fn is_confirm_delete_shown(&self) -> bool {
|
||||||
self.confirm_delete_message_id.is_some()
|
self.confirm_delete_message_id.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Начать режим ответа на выбранное сообщение
|
||||||
|
pub fn start_reply_to_selected(&mut self) -> bool {
|
||||||
|
if let Some(msg) = self.get_selected_message() {
|
||||||
|
self.replying_to_message_id = Some(msg.id);
|
||||||
|
self.selected_message_index = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Отменить режим ответа
|
||||||
|
pub fn cancel_reply(&mut self) {
|
||||||
|
self.replying_to_message_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверить, находимся ли в режиме ответа
|
||||||
|
pub fn is_replying(&self) -> bool {
|
||||||
|
self.replying_to_message_id.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить сообщение, на которое отвечаем
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Начать режим пересылки выбранного сообщения
|
||||||
|
pub fn start_forward_selected(&mut self) -> bool {
|
||||||
|
if let Some(msg) = self.get_selected_message() {
|
||||||
|
self.forwarding_message_id = Some(msg.id);
|
||||||
|
self.selected_message_index = None;
|
||||||
|
self.is_selecting_forward_chat = true;
|
||||||
|
// Сбрасываем выбор чата на первый
|
||||||
|
self.chat_list_state.select(Some(0));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Отменить режим пересылки
|
||||||
|
pub fn cancel_forward(&mut self) {
|
||||||
|
self.forwarding_message_id = None;
|
||||||
|
self.is_selecting_forward_chat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверить, находимся ли в режиме выбора чата для пересылки
|
||||||
|
pub fn is_forwarding(&self) -> bool {
|
||||||
|
self.is_selecting_forward_chat && self.forwarding_message_id.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить сообщение для пересылки
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,51 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Режим выбора чата для пересылки
|
||||||
|
if app.is_forwarding() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.cancel_forward();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
// Выбираем чат и пересылаем сообщение
|
||||||
|
let filtered = app.get_filtered_chats();
|
||||||
|
if let Some(i) = app.chat_list_state.selected() {
|
||||||
|
if let Some(chat) = filtered.get(i) {
|
||||||
|
let to_chat_id = chat.id;
|
||||||
|
if let Some(msg_id) = app.forwarding_message_id {
|
||||||
|
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 {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
|
app.status_message = Some("Сообщение переслано".to_string());
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
app.error_message = Some(e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
app.error_message = Some("Таймаут пересылки".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.cancel_forward();
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
app.next_chat();
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
app.previous_chat();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Режим поиска
|
// Режим поиска
|
||||||
if app.is_searching {
|
if app.is_searching {
|
||||||
match key.code {
|
match key.code {
|
||||||
@@ -81,7 +126,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
app.message_scroll_offset = 0;
|
app.message_scroll_offset = 0;
|
||||||
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
// Сообщения уже сохранены в td_client.current_chat_messages
|
// Загружаем недостающие reply info
|
||||||
|
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await;
|
||||||
app.status_message = None;
|
app.status_message = None;
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -161,11 +207,21 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Обычная отправка
|
// Обычная отправка (или reply)
|
||||||
|
let reply_to_id = app.replying_to_message_id;
|
||||||
|
// Создаём ReplyInfo ДО отправки, пока сообщение точно доступно
|
||||||
|
let reply_info = app.get_replying_to_message().map(|m| {
|
||||||
|
crate::tdlib::client::ReplyInfo {
|
||||||
|
message_id: m.id,
|
||||||
|
sender_name: m.sender_name.clone(),
|
||||||
|
text: m.content.clone(),
|
||||||
|
}
|
||||||
|
});
|
||||||
app.message_input.clear();
|
app.message_input.clear();
|
||||||
app.cursor_position = 0;
|
app.cursor_position = 0;
|
||||||
|
app.replying_to_message_id = None;
|
||||||
|
|
||||||
match timeout(Duration::from_secs(5), app.td_client.send_message(chat_id, text)).await {
|
match timeout(Duration::from_secs(5), app.td_client.send_message(chat_id, text, reply_to_id, reply_info)).await {
|
||||||
Ok(Ok(sent_msg)) => {
|
Ok(Ok(sent_msg)) => {
|
||||||
// Добавляем отправленное сообщение в список (с лимитом)
|
// Добавляем отправленное сообщение в список (с лимитом)
|
||||||
app.td_client.push_message(sent_msg);
|
app.td_client.push_message(sent_msg);
|
||||||
@@ -193,7 +249,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
app.message_scroll_offset = 0;
|
app.message_scroll_offset = 0;
|
||||||
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
// Сообщения уже сохранены в td_client.current_chat_messages
|
// Загружаем недостающие reply info
|
||||||
|
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await;
|
||||||
app.status_message = None;
|
app.status_message = None;
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -211,7 +268,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Esc - отменить выбор/редактирование или закрыть чат
|
// Esc - отменить выбор/редактирование/reply или закрыть чат
|
||||||
if key.code == KeyCode::Esc {
|
if key.code == KeyCode::Esc {
|
||||||
if app.is_selecting_message() {
|
if app.is_selecting_message() {
|
||||||
// Отменить выбор сообщения
|
// Отменить выбор сообщения
|
||||||
@@ -219,6 +276,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
} else if app.is_editing() {
|
} else if app.is_editing() {
|
||||||
// Отменить редактирование
|
// Отменить редактирование
|
||||||
app.cancel_editing();
|
app.cancel_editing();
|
||||||
|
} else if app.is_replying() {
|
||||||
|
// Отменить режим ответа
|
||||||
|
app.cancel_reply();
|
||||||
} else if app.selected_chat_id.is_some() {
|
} else if app.selected_chat_id.is_some() {
|
||||||
app.close_chat();
|
app.close_chat();
|
||||||
}
|
}
|
||||||
@@ -246,6 +306,14 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('r') | KeyCode::Char('к') => {
|
||||||
|
// Начать режим ответа на выбранное сообщение
|
||||||
|
app.start_reply_to_selected();
|
||||||
|
}
|
||||||
|
KeyCode::Char('f') | KeyCode::Char('а') => {
|
||||||
|
// Начать режим пересылки
|
||||||
|
app.start_forward_selected();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -113,6 +113,26 @@ pub struct ChatInfo {
|
|||||||
pub is_muted: bool,
|
pub is_muted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Информация о сообщении, на которое отвечают
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReplyInfo {
|
||||||
|
/// ID сообщения, на которое отвечают
|
||||||
|
pub message_id: i64,
|
||||||
|
/// Имя отправителя оригинального сообщения
|
||||||
|
pub sender_name: String,
|
||||||
|
/// Текст оригинального сообщения (превью)
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Информация о пересланном сообщении
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ForwardInfo {
|
||||||
|
/// Имя оригинального отправителя
|
||||||
|
pub sender_name: String,
|
||||||
|
/// Дата оригинального сообщения
|
||||||
|
pub date: i32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MessageInfo {
|
pub struct MessageInfo {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -131,6 +151,10 @@ pub struct MessageInfo {
|
|||||||
pub can_be_deleted_only_for_self: bool,
|
pub can_be_deleted_only_for_self: bool,
|
||||||
/// Можно ли удалить для всех
|
/// Можно ли удалить для всех
|
||||||
pub can_be_deleted_for_all_users: bool,
|
pub can_be_deleted_for_all_users: bool,
|
||||||
|
/// Информация о reply (если это ответ на сообщение)
|
||||||
|
pub reply_to: Option<ReplyInfo>,
|
||||||
|
/// Информация о forward (если сообщение переслано)
|
||||||
|
pub forward_from: Option<ForwardInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -240,7 +264,17 @@ impl TdClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет сообщение в текущий чат с соблюдением лимита
|
/// Добавляет сообщение в текущий чат с соблюдением лимита
|
||||||
|
/// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to)
|
||||||
pub fn push_message(&mut self, msg: MessageInfo) {
|
pub fn push_message(&mut self, msg: MessageInfo) {
|
||||||
|
// Проверяем, есть ли уже сообщение с таким 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;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.current_chat_messages.push(msg);
|
self.current_chat_messages.push(msg);
|
||||||
// Ограничиваем количество сообщений (удаляем старые)
|
// Ограничиваем количество сообщений (удаляем старые)
|
||||||
if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT {
|
if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT {
|
||||||
@@ -389,12 +423,25 @@ impl TdClient {
|
|||||||
let msg_info = self.convert_message(&new_msg.message, chat_id);
|
let msg_info = self.convert_message(&new_msg.message, chat_id);
|
||||||
let msg_id = msg_info.id;
|
let msg_id = msg_info.id;
|
||||||
let is_incoming = !msg_info.is_outgoing;
|
let is_incoming = !msg_info.is_outgoing;
|
||||||
// Проверяем, что сообщение ещё не добавлено (по id)
|
|
||||||
if !self.current_chat_messages.iter().any(|m| m.id == msg_info.id) {
|
// Проверяем, есть ли уже сообщение с таким id
|
||||||
self.push_message(msg_info);
|
let existing_idx = self.current_chat_messages.iter().position(|m| m.id == msg_info.id);
|
||||||
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
|
|
||||||
if is_incoming {
|
match existing_idx {
|
||||||
self.pending_view_messages.push((chat_id, vec![msg_id]));
|
Some(idx) => {
|
||||||
|
// Сообщение уже есть - обновляем только если входящее
|
||||||
|
// (исходящие уже добавлены через send_message с правильным reply_to)
|
||||||
|
if is_incoming {
|
||||||
|
self.current_chat_messages[idx] = msg_info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Нового сообщения нет - добавляем
|
||||||
|
self.push_message(msg_info);
|
||||||
|
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
|
||||||
|
if is_incoming {
|
||||||
|
self.pending_view_messages.push((chat_id, vec![msg_id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,6 +677,12 @@ impl TdClient {
|
|||||||
|
|
||||||
let (content, entities) = extract_message_text_static(message);
|
let (content, entities) = extract_message_text_static(message);
|
||||||
|
|
||||||
|
// Извлекаем информацию о reply
|
||||||
|
let reply_to = self.extract_reply_info(message);
|
||||||
|
|
||||||
|
// Извлекаем информацию о forward
|
||||||
|
let forward_from = self.extract_forward_info(message);
|
||||||
|
|
||||||
MessageInfo {
|
MessageInfo {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
sender_name,
|
sender_name,
|
||||||
@@ -642,6 +695,187 @@ impl TdClient {
|
|||||||
can_be_edited: message.can_be_edited,
|
can_be_edited: message.can_be_edited,
|
||||||
can_be_deleted_only_for_self: message.can_be_deleted_only_for_self,
|
can_be_deleted_only_for_self: message.can_be_deleted_only_for_self,
|
||||||
can_be_deleted_for_all_users: message.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: message.can_be_deleted_for_all_users,
|
||||||
|
reply_to,
|
||||||
|
forward_from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Извлекает информацию о reply из сообщения
|
||||||
|
fn extract_reply_info(&self, message: &TdMessage) -> Option<ReplyInfo> {
|
||||||
|
use tdlib_rs::enums::MessageReplyTo;
|
||||||
|
|
||||||
|
match &message.reply_to {
|
||||||
|
Some(MessageReplyTo::Message(reply)) => {
|
||||||
|
// Получаем имя отправителя из origin или ищем сообщение в текущем списке
|
||||||
|
let sender_name = if let Some(origin) = &reply.origin {
|
||||||
|
self.get_origin_sender_name(origin)
|
||||||
|
} else {
|
||||||
|
// Пробуем найти оригинальное сообщение в текущем списке
|
||||||
|
self.current_chat_messages
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.id == reply.message_id)
|
||||||
|
.map(|m| m.sender_name.clone())
|
||||||
|
.unwrap_or_else(|| "...".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем текст из content или quote
|
||||||
|
let text = if let Some(quote) = &reply.quote {
|
||||||
|
quote.text.text.clone()
|
||||||
|
} else if let Some(content) = &reply.content {
|
||||||
|
extract_content_text(content)
|
||||||
|
} else {
|
||||||
|
// Пробуем найти в текущих сообщениях
|
||||||
|
self.current_chat_messages
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.id == reply.message_id)
|
||||||
|
.map(|m| m.content.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ReplyInfo {
|
||||||
|
message_id: reply.message_id,
|
||||||
|
sender_name,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Извлекает информацию о forward из сообщения
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получает имя отправителя из MessageOrigin
|
||||||
|
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::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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновляет reply info для сообщений, где данные не были загружены
|
||||||
|
/// Вызывается после загрузки истории, когда все сообщения уже в списке
|
||||||
|
fn update_reply_info_from_loaded_messages(&mut self) {
|
||||||
|
// Собираем данные для обновления (id -> (sender_name, content))
|
||||||
|
let msg_data: std::collections::HashMap<i64, (String, String)> = self
|
||||||
|
.current_chat_messages
|
||||||
|
.iter()
|
||||||
|
.map(|m| (m.id, (m.sender_name.clone(), m.content.clone())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Обновляем reply_to для сообщений с неполными данными
|
||||||
|
for msg in &mut self.current_chat_messages {
|
||||||
|
if let Some(ref mut reply) = msg.reply_to {
|
||||||
|
// Если sender_name = "..." или text пустой — пробуем заполнить
|
||||||
|
if reply.sender_name == "..." || reply.text.is_empty() {
|
||||||
|
if let Some((sender, content)) = msg_data.get(&reply.message_id) {
|
||||||
|
if reply.sender_name == "..." {
|
||||||
|
reply.sender_name = sender.clone();
|
||||||
|
}
|
||||||
|
if reply.text.is_empty() {
|
||||||
|
reply.text = content.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Асинхронно обновляет reply info, загружая недостающие сообщения
|
||||||
|
pub async fn fetch_missing_reply_info(&mut self) {
|
||||||
|
let chat_id = match self.current_chat_id {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Собираем message_id для которых нужно загрузить данные
|
||||||
|
let missing_ids: Vec<i64> = self
|
||||||
|
.current_chat_messages
|
||||||
|
.iter()
|
||||||
|
.filter_map(|msg| {
|
||||||
|
msg.reply_to.as_ref().and_then(|reply| {
|
||||||
|
if reply.sender_name == "..." || reply.text.is_empty() {
|
||||||
|
Some(reply.message_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if missing_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем каждое сообщение и кэшируем данные
|
||||||
|
let mut reply_cache: std::collections::HashMap<i64, (String, String)> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for msg_id in missing_ids {
|
||||||
|
if reply_cache.contains_key(&msg_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(tdlib_rs::enums::Message::Message(msg)) =
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (content, _) = extract_message_text_static(&msg);
|
||||||
|
reply_cache.insert(msg_id, (sender_name, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяем загруженные данные
|
||||||
|
for msg in &mut self.current_chat_messages {
|
||||||
|
if let Some(ref mut reply) = msg.reply_to {
|
||||||
|
if let Some((sender, content)) = reply_cache.get(&reply.message_id) {
|
||||||
|
if reply.sender_name == "..." {
|
||||||
|
reply.sender_name = sender.clone();
|
||||||
|
}
|
||||||
|
if reply.text.is_empty() {
|
||||||
|
reply.text = content.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,6 +1013,9 @@ impl TdClient {
|
|||||||
all_messages.reverse();
|
all_messages.reverse();
|
||||||
self.current_chat_messages = all_messages.clone();
|
self.current_chat_messages = all_messages.clone();
|
||||||
|
|
||||||
|
// Обновляем reply info для сообщений где данные не были загружены
|
||||||
|
self.update_reply_info_from_loaded_messages();
|
||||||
|
|
||||||
// Отмечаем сообщения как прочитанные
|
// Отмечаем сообщения как прочитанные
|
||||||
if !all_messages.is_empty() {
|
if !all_messages.is_empty() {
|
||||||
let message_ids: Vec<i64> = all_messages.iter().map(|m| m.id).collect();
|
let message_ids: Vec<i64> = all_messages.iter().map(|m| m.id).collect();
|
||||||
@@ -860,10 +1097,10 @@ impl TdClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Отправка текстового сообщения с поддержкой Markdown
|
/// Отправка текстового сообщения с поддержкой Markdown и reply
|
||||||
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<MessageInfo, String> {
|
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};
|
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage};
|
||||||
use tdlib_rs::enums::{InputMessageContent, TextParseMode};
|
use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo};
|
||||||
|
|
||||||
// Парсим markdown в тексте
|
// Парсим markdown в тексте
|
||||||
let formatted_text = match functions::parse_text_entities(
|
let formatted_text = match functions::parse_text_entities(
|
||||||
@@ -890,10 +1127,20 @@ impl TdClient {
|
|||||||
clear_draft: true,
|
clear_draft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Создаём reply_to если есть message_id для ответа
|
||||||
|
// chat_id: 0 означает ответ в том же чате
|
||||||
|
let reply_to = reply_to_message_id.map(|msg_id| {
|
||||||
|
InputMessageReplyTo::Message(InputMessageReplyToMessage {
|
||||||
|
chat_id: 0,
|
||||||
|
message_id: msg_id,
|
||||||
|
quote: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let result = functions::send_message(
|
let result = functions::send_message(
|
||||||
chat_id,
|
chat_id,
|
||||||
0, // message_thread_id
|
0, // message_thread_id
|
||||||
None, // reply_to
|
reply_to,
|
||||||
None, // options
|
None, // options
|
||||||
content,
|
content,
|
||||||
self.client_id,
|
self.client_id,
|
||||||
@@ -904,6 +1151,7 @@ impl TdClient {
|
|||||||
Ok(tdlib_rs::enums::Message::Message(msg)) => {
|
Ok(tdlib_rs::enums::Message::Message(msg)) => {
|
||||||
// Извлекаем текст и entities из отправленного сообщения
|
// Извлекаем текст и entities из отправленного сообщения
|
||||||
let (content, entities) = extract_message_text_static(&msg);
|
let (content, entities) = extract_message_text_static(&msg);
|
||||||
|
|
||||||
Ok(MessageInfo {
|
Ok(MessageInfo {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
sender_name: "Вы".to_string(),
|
sender_name: "Вы".to_string(),
|
||||||
@@ -916,6 +1164,8 @@ impl TdClient {
|
|||||||
can_be_edited: msg.can_be_edited,
|
can_be_edited: msg.can_be_edited,
|
||||||
can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self,
|
can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self,
|
||||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||||
|
reply_to: reply_info,
|
||||||
|
forward_from: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
||||||
@@ -975,6 +1225,8 @@ impl TdClient {
|
|||||||
can_be_edited: msg.can_be_edited,
|
can_be_edited: msg.can_be_edited,
|
||||||
can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self,
|
can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self,
|
||||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||||
|
reply_to: None, // При редактировании reply сохраняется из оригинала
|
||||||
|
forward_from: None, // При редактировании forward сохраняется из оригинала
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)),
|
||||||
@@ -998,6 +1250,26 @@ impl TdClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Пересылка сообщений
|
||||||
|
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
|
||||||
|
false, // send_copy
|
||||||
|
false, // remove_caption
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Ошибка пересылки сообщения: {:?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Обработка очереди сообщений для отметки как прочитанных
|
/// Обработка очереди сообщений для отметки как прочитанных
|
||||||
pub async fn process_pending_view_messages(&mut self) {
|
pub async fn process_pending_view_messages(&mut self) {
|
||||||
let pending = std::mem::take(&mut self.pending_view_messages);
|
let pending = std::mem::take(&mut self.pending_view_messages);
|
||||||
@@ -1125,3 +1397,33 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
|
|||||||
_ => ("[Сообщение]".to_string(), vec![]),
|
_ => ("[Сообщение]".to_string(), vec![]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Извлекает текст из MessageContent (для reply preview)
|
||||||
|
fn extract_content_text(content: &MessageContent) -> String {
|
||||||
|
match content {
|
||||||
|
MessageContent::MessageText(text) => text.text.text.clone(),
|
||||||
|
MessageContent::MessagePhoto(photo) => {
|
||||||
|
if photo.caption.text.is_empty() {
|
||||||
|
"[Фото]".to_string()
|
||||||
|
} else {
|
||||||
|
format!("[Фото] {}", photo.caption.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageContent::MessageVideo(video) => {
|
||||||
|
if video.caption.text.is_empty() {
|
||||||
|
"[Видео]".to_string()
|
||||||
|
} else {
|
||||||
|
format!("[Видео] {}", video.caption.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageContent::MessageDocument(doc) => format!("[Файл: {}]", doc.document.file_name),
|
||||||
|
MessageContent::MessageVoiceNote(_) => "[Голосовое]".to_string(),
|
||||||
|
MessageContent::MessageVideoNote(_) => "[Видеосообщение]".to_string(),
|
||||||
|
MessageContent::MessageSticker(sticker) => format!("[Стикер: {}]", sticker.sticker.emoji),
|
||||||
|
MessageContent::MessageAnimation(_) => "[GIF]".to_string(),
|
||||||
|
MessageContent::MessageAudio(audio) => format!("[Аудио: {}]", audio.audio.title),
|
||||||
|
MessageContent::MessageCall(_) => "[Звонок]".to_string(),
|
||||||
|
MessageContent::MessagePoll(poll) => format!("[Опрос: {}]", poll.poll.question.text),
|
||||||
|
_ => "[Сообщение]".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,8 +83,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Заголовок блока: обычный или режим пересылки
|
||||||
|
let block = if app.is_forwarding() {
|
||||||
|
Block::default()
|
||||||
|
.title(" ↪ Выберите чат ")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan))
|
||||||
|
} else {
|
||||||
|
Block::default().borders(Borders::ALL)
|
||||||
|
};
|
||||||
|
|
||||||
let chats_list = List::new(items)
|
let chats_list = List::new(items)
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.block(block)
|
||||||
.highlight_style(
|
.highlight_style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::ITALIC)
|
.add_modifier(Modifier::ITALIC)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Рендерит текст инпута с блочным курсором
|
/// Рендерит текст инпута с блочным курсором
|
||||||
fn render_input_with_cursor<'a>(prefix: &'a str, text: &str, cursor_pos: usize, color: Color) -> Line<'a> {
|
fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: Color) -> Line<'static> {
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
|
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
|
||||||
|
|
||||||
@@ -435,6 +435,48 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let selection_marker = if is_selected { "▶ " } else { "" };
|
let selection_marker = if is_selected { "▶ " } else { "" };
|
||||||
let marker_len = selection_marker.chars().count();
|
let marker_len = selection_marker.chars().count();
|
||||||
|
|
||||||
|
// Отображаем forward если есть
|
||||||
|
if let Some(forward) = &msg.forward_from {
|
||||||
|
let forward_line = format!("↪ Переслано от {}", forward.sender_name);
|
||||||
|
let forward_len = forward_line.chars().count();
|
||||||
|
|
||||||
|
if msg.is_outgoing {
|
||||||
|
// Forward справа для исходящих
|
||||||
|
let padding = content_width.saturating_sub(forward_len + 1);
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::raw(" ".repeat(padding)),
|
||||||
|
Span::styled(forward_line, Style::default().fg(Color::Magenta)),
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
// Forward слева для входящих
|
||||||
|
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 reply_line = format!("┌ {}: {}{}", reply.sender_name, reply_text, ellipsis);
|
||||||
|
let reply_len = reply_line.chars().count();
|
||||||
|
|
||||||
|
if msg.is_outgoing {
|
||||||
|
// Reply справа для исходящих
|
||||||
|
let padding = content_width.saturating_sub(reply_len + 1);
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::raw(" ".repeat(padding)),
|
||||||
|
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
// Reply слева для входящих
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if msg.is_outgoing {
|
if msg.is_outgoing {
|
||||||
// Исходящие: справа, формат "текст (HH:MM ✎ ✓✓)"
|
// Исходящие: справа, формат "текст (HH:MM ✎ ✓✓)"
|
||||||
let read_mark = if msg.is_read { "✓✓" } else { "✓" };
|
let read_mark = if msg.is_read { "✓✓" } else { "✓" };
|
||||||
@@ -562,17 +604,29 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
f.render_widget(messages_widget, message_chunks[1]);
|
f.render_widget(messages_widget, message_chunks[1]);
|
||||||
|
|
||||||
// Input box с wrap для длинного текста и блочным курсором
|
// Input box с wrap для длинного текста и блочным курсором
|
||||||
let (input_line, input_title) = if app.is_selecting_message() {
|
let (input_line, input_title) = if app.is_forwarding() {
|
||||||
|
// Режим пересылки - показываем превью сообщения
|
||||||
|
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 { "" };
|
||||||
|
format!("↪ {}{}", text_preview, ellipsis)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "↪ ...".to_string());
|
||||||
|
|
||||||
|
let line = Line::from(Span::styled(forward_preview, Style::default().fg(Color::Cyan)));
|
||||||
|
(line, " Выберите чат ← ")
|
||||||
|
} else if app.is_selecting_message() {
|
||||||
// Режим выбора сообщения - подсказка зависит от возможностей
|
// Режим выбора сообщения - подсказка зависит от возможностей
|
||||||
let selected_msg = app.get_selected_message();
|
let selected_msg = app.get_selected_message();
|
||||||
let can_edit = selected_msg.map(|m| m.can_be_edited && m.is_outgoing).unwrap_or(false);
|
let can_edit = selected_msg.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_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
|
||||||
|
|
||||||
let hint = match (can_edit, can_delete) {
|
let hint = match (can_edit, can_delete) {
|
||||||
(true, true) => "↑↓ выбрать · Enter редакт. · d удалить · Esc отмена",
|
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · d удал. · Esc",
|
||||||
(true, false) => "↑↓ выбрать · Enter редакт. · Esc отмена",
|
(true, false) => "↑↓ · Enter ред. · r ответ · f переслть · Esc",
|
||||||
(false, true) => "↑↓ выбрать · d удалить · Esc отмена",
|
(false, true) => "↑↓ · r ответ · f переслать · d удалить · Esc",
|
||||||
(false, false) => "↑↓ выбрать · Esc отмена",
|
(false, false) => "↑↓ · r ответить · f переслать · Esc",
|
||||||
};
|
};
|
||||||
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
||||||
} else if app.is_editing() {
|
} else if app.is_editing() {
|
||||||
@@ -590,6 +644,31 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let line = render_input_with_cursor("✏ ", &app.message_input, app.cursor_position, Color::Magenta);
|
let line = render_input_with_cursor("✏ ", &app.message_input, app.cursor_position, Color::Magenta);
|
||||||
(line, " Редактирование (Esc отмена) ")
|
(line, " Редактирование (Esc отмена) ")
|
||||||
}
|
}
|
||||||
|
} else if app.is_replying() {
|
||||||
|
// Режим ответа на сообщение
|
||||||
|
let reply_preview = app.get_replying_to_message()
|
||||||
|
.map(|m| {
|
||||||
|
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 { "" };
|
||||||
|
format!("{}: {}{}", sender, text_preview, ellipsis)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "...".to_string());
|
||||||
|
|
||||||
|
if app.message_input.is_empty() {
|
||||||
|
let line = Line::from(vec![
|
||||||
|
Span::styled("↪ ", Style::default().fg(Color::Cyan)),
|
||||||
|
Span::styled(reply_preview, Style::default().fg(Color::Gray)),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled("█", Style::default().fg(Color::Yellow)),
|
||||||
|
]);
|
||||||
|
(line, " Ответ (Esc отмена) ")
|
||||||
|
} 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);
|
||||||
|
(line, " Ответ (Esc отмена) ")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Обычный режим
|
// Обычный режим
|
||||||
if app.message_input.is_empty() {
|
if app.message_input.is_empty() {
|
||||||
@@ -610,10 +689,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let input_block = if input_title.is_empty() {
|
let input_block = if input_title.is_empty() {
|
||||||
Block::default().borders(Borders::ALL)
|
Block::default().borders(Borders::ALL)
|
||||||
} else {
|
} else {
|
||||||
|
let title_color = if app.is_replying() || app.is_forwarding() {
|
||||||
|
Color::Cyan
|
||||||
|
} else {
|
||||||
|
Color::Magenta
|
||||||
|
};
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title(input_title)
|
.title(input_title)
|
||||||
.title_style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD))
|
.title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD))
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = Paragraph::new(input_line)
|
let input = Paragraph::new(input_line)
|
||||||
|
|||||||
Reference in New Issue
Block a user