fixes
This commit is contained in:
38
CONTEXT.md
38
CONTEXT.md
@@ -14,20 +14,26 @@
|
|||||||
#### Функциональность
|
#### Функциональность
|
||||||
- Загрузка списка чатов (до 50 штук)
|
- Загрузка списка чатов (до 50 штук)
|
||||||
- **Фильтрация чатов**: показываются только чаты из ChatList::Main (без архива)
|
- **Фильтрация чатов**: показываются только чаты из ChatList::Main (без архива)
|
||||||
- Отображение названия чата и счётчика непрочитанных
|
- Отображение названия чата, счётчика непрочитанных и **@username**
|
||||||
- Загрузка истории сообщений при открытии чата
|
- **Иконка 📌** для закреплённых чатов
|
||||||
- Отображение сообщений с именем отправителя и временем
|
- Загрузка истории сообщений при открытии чата (множественные попытки)
|
||||||
|
- **Группировка сообщений по дате** (разделители "Сегодня", "Вчера", дата)
|
||||||
|
- **Группировка сообщений по отправителю** (заголовок с именем)
|
||||||
|
- **Отображение времени сообщений** в формате [HH:MM]
|
||||||
|
- **Галочки прочтения** (✓ отправлено, ✓✓ прочитано)
|
||||||
- **Отправка текстовых сообщений**
|
- **Отправка текстовых сообщений**
|
||||||
- **Поиск по чатам** (Ctrl+S): фильтрация списка по названию
|
- **Новые сообщения в реальном времени** при открытом чате
|
||||||
|
- **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username
|
||||||
|
|
||||||
#### Управление
|
#### Управление
|
||||||
- `↑/↓` стрелки — навигация по списку чатов
|
- `↑/↓` стрелки — навигация по списку чатов
|
||||||
- `Enter` — открыть чат / отправить сообщение
|
- `Enter` — открыть чат / отправить сообщение
|
||||||
- `Esc` — закрыть открытый чат / отменить поиск
|
- `Esc` — закрыть открытый чат / отменить поиск
|
||||||
- `Ctrl+S` — поиск по чатам (фильтрация по названию)
|
- `Ctrl+S` — поиск по чатам (фильтрация по названию и username)
|
||||||
- `Ctrl+R` — обновить список чатов
|
- `Ctrl+R` — обновить список чатов
|
||||||
- `Ctrl+C` — выход
|
- `Ctrl+C` — выход
|
||||||
- `Cmd+↑/Cmd+↓` — скролл сообщений в открытом чате
|
- `Cmd+↑/Cmd+↓` — скролл сообщений в открытом чате (с подгрузкой старых)
|
||||||
|
- `1-9` — переключение папок (в списке чатов)
|
||||||
- Ввод текста в поле сообщения
|
- Ввод текста в поле сообщения
|
||||||
|
|
||||||
### Структура проекта
|
### Структура проекта
|
||||||
@@ -43,17 +49,17 @@ src/
|
|||||||
│ ├── loading.rs # Экран загрузки
|
│ ├── loading.rs # Экран загрузки
|
||||||
│ ├── auth.rs # Экран авторизации
|
│ ├── auth.rs # Экран авторизации
|
||||||
│ ├── main_screen.rs # Главный экран
|
│ ├── main_screen.rs # Главный экран
|
||||||
│ ├── chat_list.rs # Список чатов
|
│ ├── chat_list.rs # Список чатов (с pin и username)
|
||||||
│ ├── messages.rs # Область сообщений
|
│ ├── messages.rs # Область сообщений (группировка по дате/отправителю)
|
||||||
│ └── footer.rs # Подвал с командами
|
│ └── footer.rs # Подвал с командами
|
||||||
├── input/
|
├── input/
|
||||||
│ ├── mod.rs # Роутинг ввода
|
│ ├── mod.rs # Роутинг ввода
|
||||||
│ ├── auth.rs # Обработка ввода на экране авторизации
|
│ ├── auth.rs # Обработка ввода на экране авторизации
|
||||||
│ └── main_input.rs # Обработка ввода на главном экране
|
│ └── main_input.rs # Обработка ввода на главном экране
|
||||||
├── utils.rs # Утилиты (disable_tdlib_logs, format_timestamp)
|
├── utils.rs # Утилиты (disable_tdlib_logs, format_timestamp, format_date, get_day)
|
||||||
└── tdlib/
|
└── tdlib/
|
||||||
├── mod.rs # Модуль экспорта
|
├── mod.rs # Модуль экспорта
|
||||||
└── client.rs # TdClient: авторизация, загрузка чатов, сообщений, отправка
|
└── client.rs # TdClient: авторизация, чаты, сообщения, кеш usernames
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ключевые решения
|
### Ключевые решения
|
||||||
@@ -64,9 +70,11 @@ src/
|
|||||||
|
|
||||||
3. **Синхронизация чатов**: Чаты загружаются асинхронно через updates. Main loop периодически синхронизирует `app.chats` с `td_client.chats`.
|
3. **Синхронизация чатов**: Чаты загружаются асинхронно через updates. Main loop периодически синхронизирует `app.chats` с `td_client.chats`.
|
||||||
|
|
||||||
4. **Фильтрация чатов**: Все чаты добавляются в список при получении `NewChat` update. Позиции обновляются через `ChatPosition` update.
|
4. **Кеширование usernames**: При получении `Update::User` сохраняем username в HashMap. При получении приватного чата связываем chat_id с user_id.
|
||||||
|
|
||||||
5. **Сортировка по TDLib order**: Используем `position.order` для сортировки чатов (учитывает pinned и время).
|
5. **Группировка сообщений**: Сообщения группируются по дате (разделители) и по отправителю (заголовки). Время отображается рядом с каждым сообщением.
|
||||||
|
|
||||||
|
6. **Новые сообщения**: `current_chat_id` отслеживает открытый чат. При получении `NewMessage` для этого чата сообщение добавляется сразу.
|
||||||
|
|
||||||
### Зависимости (Cargo.toml)
|
### Зависимости (Cargo.toml)
|
||||||
|
|
||||||
@@ -92,12 +100,10 @@ API_HASH=your_api_hash
|
|||||||
- [ ] Папки телеграма (сейчас только "All")
|
- [ ] Папки телеграма (сейчас только "All")
|
||||||
- [ ] Отображение онлайн-статуса пользователя
|
- [ ] Отображение онлайн-статуса пользователя
|
||||||
- [ ] Markdown форматирование в сообщениях
|
- [ ] Markdown форматирование в сообщениях
|
||||||
- [ ] Скролл истории сообщений (больше 50 сообщений)
|
|
||||||
- [ ] Отметка сообщений как прочитанные
|
- [ ] Отметка сообщений как прочитанные
|
||||||
- [ ] Обновление чатов в реальном времени (новые сообщения)
|
- [ ] Медиа-сообщения (фото, видео, голосовые)
|
||||||
- [ ] Загрузка имён пользователей (сейчас показывается User_ID)
|
|
||||||
|
|
||||||
## Известные проблемы
|
## Известные проблемы
|
||||||
|
|
||||||
1. При первом запуске нужно пройти авторизацию
|
1. При первом запуске нужно пройти авторизацию
|
||||||
2. Имя отправителя показывается как "User_ID" (нужно загружать имена пользователей)
|
2. Время отображается с фиксированным смещением +3 (MSK)
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ impl App {
|
|||||||
self.current_messages.clear();
|
self.current_messages.clear();
|
||||||
self.message_input.clear();
|
self.message_input.clear();
|
||||||
self.message_scroll_offset = 0;
|
self.message_scroll_offset = 0;
|
||||||
|
self.td_client.current_chat_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_first_chat(&mut self) {
|
pub fn select_first_chat(&mut self) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||||
app.message_scroll_offset = 0;
|
app.message_scroll_offset = 0;
|
||||||
match timeout(Duration::from_secs(5), app.td_client.get_chat_history(chat_id, 50)).await {
|
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
||||||
Ok(Ok(messages)) => {
|
Ok(Ok(messages)) => {
|
||||||
app.current_messages = messages;
|
app.current_messages = messages;
|
||||||
app.status_message = None;
|
app.status_message = None;
|
||||||
@@ -160,7 +160,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||||
app.message_scroll_offset = 0;
|
app.message_scroll_offset = 0;
|
||||||
match timeout(Duration::from_secs(5), app.td_client.get_chat_history(chat_id, 50)).await {
|
match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await {
|
||||||
Ok(Ok(messages)) => {
|
Ok(Ok(messages)) => {
|
||||||
app.current_messages = messages;
|
app.current_messages = messages;
|
||||||
app.status_message = None;
|
app.status_message = None;
|
||||||
|
|||||||
15
src/main.rs
15
src/main.rs
@@ -104,6 +104,21 @@ async fn run_app<B: ratatui::backend::Backend>(
|
|||||||
app.td_client.handle_update(update);
|
app.td_client.handle_update(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обрабатываем очередь сообщений для отметки как прочитанных
|
||||||
|
if !app.td_client.pending_view_messages.is_empty() {
|
||||||
|
app.td_client.process_pending_view_messages().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронизируем сообщения из td_client в app (для новых сообщений в реальном времени)
|
||||||
|
if app.selected_chat_id.is_some() && !app.td_client.current_chat_messages.is_empty() {
|
||||||
|
// Добавляем новые сообщения, которых ещё нет в app.current_messages
|
||||||
|
for msg in &app.td_client.current_chat_messages {
|
||||||
|
if !app.current_messages.iter().any(|m| m.id == msg.id) {
|
||||||
|
app.current_messages.push(msg.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем состояние экрана на основе auth_state
|
// Обновляем состояние экрана на основе auth_state
|
||||||
update_screen_state(app).await;
|
update_screen_state(app).await;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub struct ChatInfo {
|
|||||||
pub unread_count: i32,
|
pub unread_count: i32,
|
||||||
pub is_pinned: bool,
|
pub is_pinned: bool,
|
||||||
pub order: i64,
|
pub order: i64,
|
||||||
|
/// ID последнего прочитанного исходящего сообщения (для галочек)
|
||||||
|
pub last_read_outbox_message_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -46,10 +48,14 @@ pub struct TdClient {
|
|||||||
client_id: i32,
|
client_id: i32,
|
||||||
pub chats: Vec<ChatInfo>,
|
pub chats: Vec<ChatInfo>,
|
||||||
pub current_chat_messages: Vec<MessageInfo>,
|
pub current_chat_messages: Vec<MessageInfo>,
|
||||||
|
/// ID текущего открытого чата (для получения новых сообщений)
|
||||||
|
pub current_chat_id: Option<i64>,
|
||||||
/// Кэш usernames: user_id -> username
|
/// Кэш usernames: user_id -> username
|
||||||
user_usernames: HashMap<i64, String>,
|
user_usernames: HashMap<i64, String>,
|
||||||
/// Связь chat_id -> user_id для приватных чатов
|
/// Связь chat_id -> user_id для приватных чатов
|
||||||
chat_user_ids: HashMap<i64, i64>,
|
chat_user_ids: HashMap<i64, i64>,
|
||||||
|
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids)
|
||||||
|
pub pending_view_messages: Vec<(i64, Vec<i64>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -70,8 +76,10 @@ impl TdClient {
|
|||||||
client_id,
|
client_id,
|
||||||
chats: Vec::new(),
|
chats: Vec::new(),
|
||||||
current_chat_messages: Vec::new(),
|
current_chat_messages: Vec::new(),
|
||||||
|
current_chat_id: None,
|
||||||
user_usernames: HashMap::new(),
|
user_usernames: HashMap::new(),
|
||||||
chat_user_ids: HashMap::new(),
|
chat_user_ids: HashMap::new(),
|
||||||
|
pending_view_messages: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +158,20 @@ impl TdClient {
|
|||||||
chat.unread_count = update.unread_count;
|
chat.unread_count = update.unread_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Update::ChatReadOutbox(update) => {
|
||||||
|
// Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения
|
||||||
|
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
|
||||||
|
chat.last_read_outbox_message_id = update.last_read_outbox_message_id;
|
||||||
|
}
|
||||||
|
// Если это текущий открытый чат — обновляем is_read у сообщений
|
||||||
|
if Some(update.chat_id) == self.current_chat_id {
|
||||||
|
for msg in &mut self.current_chat_messages {
|
||||||
|
if msg.is_outgoing && msg.id <= update.last_read_outbox_message_id {
|
||||||
|
msg.is_read = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Update::ChatPosition(update) => {
|
Update::ChatPosition(update) => {
|
||||||
// Обновляем позицию чата или удаляем его из списка
|
// Обновляем позицию чата или удаляем его из списка
|
||||||
match &update.position.list {
|
match &update.position.list {
|
||||||
@@ -171,8 +193,22 @@ impl TdClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Update::NewMessage(_new_msg) => {
|
Update::NewMessage(new_msg) => {
|
||||||
// Новые сообщения обрабатываются при обновлении UI
|
// Добавляем новое сообщение если это текущий открытый чат
|
||||||
|
let chat_id = new_msg.message.chat_id;
|
||||||
|
if Some(chat_id) == self.current_chat_id {
|
||||||
|
let msg_info = self.convert_message(&new_msg.message, chat_id);
|
||||||
|
let msg_id = msg_info.id;
|
||||||
|
let is_incoming = !msg_info.is_outgoing;
|
||||||
|
// Проверяем, что сообщение ещё не добавлено (по id)
|
||||||
|
if !self.current_chat_messages.iter().any(|m| m.id == msg_info.id) {
|
||||||
|
self.current_chat_messages.push(msg_info);
|
||||||
|
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
|
||||||
|
if is_incoming {
|
||||||
|
self.pending_view_messages.push((chat_id, vec![msg_id]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Update::User(update) => {
|
Update::User(update) => {
|
||||||
// Сохраняем username пользователя
|
// Сохраняем username пользователя
|
||||||
@@ -243,6 +279,7 @@ impl TdClient {
|
|||||||
unread_count: td_chat.unread_count,
|
unread_count: td_chat.unread_count,
|
||||||
is_pinned,
|
is_pinned,
|
||||||
order,
|
order,
|
||||||
|
last_read_outbox_message_id: td_chat.last_read_outbox_message_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) {
|
if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) {
|
||||||
@@ -250,6 +287,7 @@ impl TdClient {
|
|||||||
existing.last_message = chat_info.last_message;
|
existing.last_message = chat_info.last_message;
|
||||||
existing.last_message_date = chat_info.last_message_date;
|
existing.last_message_date = chat_info.last_message_date;
|
||||||
existing.unread_count = chat_info.unread_count;
|
existing.unread_count = chat_info.unread_count;
|
||||||
|
existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id;
|
||||||
// Обновляем username если он появился
|
// Обновляем username если он появился
|
||||||
if chat_info.username.is_some() {
|
if chat_info.username.is_some() {
|
||||||
existing.username = chat_info.username;
|
existing.username = chat_info.username;
|
||||||
@@ -267,19 +305,31 @@ impl TdClient {
|
|||||||
self.chats.sort_by(|a, b| b.order.cmp(&a.order));
|
self.chats.sort_by(|a, b| b.order.cmp(&a.order));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_message(&self, message: &TdMessage) -> MessageInfo {
|
fn convert_message(&self, message: &TdMessage, chat_id: i64) -> MessageInfo {
|
||||||
let sender_name = match &message.sender_id {
|
let sender_name = match &message.sender_id {
|
||||||
tdlib_rs::enums::MessageSender::User(user) => format!("User_{}", user.user_id),
|
tdlib_rs::enums::MessageSender::User(user) => format!("User_{}", user.user_id),
|
||||||
tdlib_rs::enums::MessageSender::Chat(chat) => format!("Chat_{}", chat.chat_id),
|
tdlib_rs::enums::MessageSender::Chat(chat) => format!("Chat_{}", chat.chat_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Определяем, прочитано ли исходящее сообщение
|
||||||
|
let is_read = if message.is_outgoing {
|
||||||
|
// Сообщение прочитано, если его ID <= last_read_outbox_message_id чата
|
||||||
|
self.chats
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.id == chat_id)
|
||||||
|
.map(|c| message.id <= c.last_read_outbox_message_id)
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
true // Входящие сообщения не показывают галочки
|
||||||
|
};
|
||||||
|
|
||||||
MessageInfo {
|
MessageInfo {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
sender_name,
|
sender_name,
|
||||||
is_outgoing: message.is_outgoing,
|
is_outgoing: message.is_outgoing,
|
||||||
content: extract_message_text_static(message),
|
content: extract_message_text_static(message),
|
||||||
date: message.date,
|
date: message.date,
|
||||||
is_read: !message.is_outgoing || message.id <= 0,
|
is_read,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,34 +389,80 @@ impl TdClient {
|
|||||||
chat_id: i64,
|
chat_id: i64,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
) -> Result<Vec<MessageInfo>, String> {
|
) -> Result<Vec<MessageInfo>, String> {
|
||||||
|
// Устанавливаем текущий чат для получения новых сообщений
|
||||||
|
self.current_chat_id = Some(chat_id);
|
||||||
let _ = functions::open_chat(chat_id, self.client_id).await;
|
let _ = functions::open_chat(chat_id, self.client_id).await;
|
||||||
|
|
||||||
// Загружаем историю с сервера (only_local=false)
|
// Пробуем загрузить несколько раз, так как сообщения могут подгружаться с сервера
|
||||||
let result = functions::get_chat_history(
|
let mut all_messages: Vec<MessageInfo> = Vec::new();
|
||||||
chat_id,
|
let mut from_message_id: i64 = 0;
|
||||||
0, // from_message_id (0 = с последнего сообщения)
|
let mut attempts = 0;
|
||||||
0, // offset
|
const MAX_ATTEMPTS: i32 = 3;
|
||||||
limit,
|
|
||||||
false, // only_local - загружаем с сервера!
|
|
||||||
self.client_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
while attempts < MAX_ATTEMPTS {
|
||||||
Ok(tdlib_rs::enums::Messages::Messages(messages)) => {
|
let result = functions::get_chat_history(
|
||||||
let mut result_messages: Vec<MessageInfo> = messages
|
chat_id,
|
||||||
.messages
|
from_message_id,
|
||||||
.into_iter()
|
0, // offset
|
||||||
.filter_map(|m| m.map(|msg| self.convert_message(&msg)))
|
limit,
|
||||||
.collect();
|
false, // only_local - загружаем с сервера!
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Сообщения приходят от новых к старым, переворачиваем
|
match result {
|
||||||
result_messages.reverse();
|
Ok(tdlib_rs::enums::Messages::Messages(messages)) => {
|
||||||
self.current_chat_messages = result_messages.clone();
|
let batch: Vec<MessageInfo> = messages
|
||||||
Ok(result_messages)
|
.messages
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|m| m.map(|msg| self.convert_message(&msg, chat_id)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if batch.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запоминаем ID самого старого сообщения для следующей загрузки
|
||||||
|
if let Some(oldest) = batch.last() {
|
||||||
|
from_message_id = oldest.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем сообщения (они приходят от новых к старым)
|
||||||
|
all_messages.extend(batch);
|
||||||
|
attempts += 1;
|
||||||
|
|
||||||
|
// Если получили достаточно сообщений, выходим
|
||||||
|
if all_messages.len() >= limit as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if all_messages.is_empty() {
|
||||||
|
return Err(format!("Ошибка загрузки сообщений: {:?}", e));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Ошибка загрузки сообщений: {:?}", e)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сообщения приходят от новых к старым, переворачиваем
|
||||||
|
all_messages.reverse();
|
||||||
|
self.current_chat_messages = all_messages.clone();
|
||||||
|
|
||||||
|
// Отмечаем сообщения как прочитанные
|
||||||
|
if !all_messages.is_empty() {
|
||||||
|
let message_ids: Vec<i64> = all_messages.iter().map(|m| m.id).collect();
|
||||||
|
let _ = functions::view_messages(
|
||||||
|
chat_id,
|
||||||
|
message_ids,
|
||||||
|
None, // source
|
||||||
|
true, // force_read
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(all_messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Загрузка старых сообщений (для скролла вверх)
|
/// Загрузка старых сообщений (для скролла вверх)
|
||||||
@@ -391,7 +487,7 @@ impl TdClient {
|
|||||||
let mut result_messages: Vec<MessageInfo> = messages
|
let mut result_messages: Vec<MessageInfo> = messages
|
||||||
.messages
|
.messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|m| m.map(|msg| self.convert_message(&msg)))
|
.filter_map(|m| m.map(|msg| self.convert_message(&msg, chat_id)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Сообщения приходят от новых к старым, переворачиваем
|
// Сообщения приходят от новых к старым, переворачиваем
|
||||||
@@ -474,6 +570,21 @@ impl TdClient {
|
|||||||
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Обработка очереди сообщений для отметки как прочитанных
|
||||||
|
pub async fn process_pending_view_messages(&mut self) {
|
||||||
|
let pending = std::mem::take(&mut self.pending_view_messages);
|
||||||
|
for (chat_id, message_ids) in pending {
|
||||||
|
let _ = functions::view_messages(
|
||||||
|
chat_id,
|
||||||
|
message_ids,
|
||||||
|
None, // source
|
||||||
|
true, // force_read
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Статическая функция для извлечения текста сообщения (без &self)
|
/// Статическая функция для извлечения текста сообщения (без &self)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|chat| {
|
.map(|chat| {
|
||||||
let is_selected = app.selected_chat_id == Some(chat.id);
|
let is_selected = app.selected_chat_id == Some(chat.id);
|
||||||
|
let pin_icon = if chat.is_pinned { "📌 " } else { "" };
|
||||||
let prefix = if is_selected { "▌ " } else { " " };
|
let prefix = if is_selected { "▌ " } else { " " };
|
||||||
|
|
||||||
let username_text = chat.username.as_ref()
|
let username_text = chat.username.as_ref()
|
||||||
@@ -54,7 +55,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = format!("{}{}{}{}", prefix, chat.title, username_text, unread_badge);
|
let content = format!("{}{}{}{}{}", prefix, pin_icon, chat.title, username_text, unread_badge);
|
||||||
let style = Style::default().fg(Color::White);
|
let style = Style::default().fg(Color::White);
|
||||||
|
|
||||||
ListItem::new(content).style(style)
|
ListItem::new(content).style(style)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::utils::format_timestamp;
|
use crate::utils::{format_timestamp, format_date, get_day};
|
||||||
|
|
||||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||||
if let Some(chat) = app.get_selected_chat() {
|
if let Some(chat) = app.get_selected_chat() {
|
||||||
@@ -20,7 +20,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Chat header
|
// Chat header
|
||||||
let header = Paragraph::new(format!("👤 {}", chat.title))
|
let header_text = match &chat.username {
|
||||||
|
Some(username) => format!("👤 {} {}", chat.title, username),
|
||||||
|
None => format!("👤 {}", chat.title),
|
||||||
|
};
|
||||||
|
let header = Paragraph::new(header_text)
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.block(Block::default().borders(Borders::ALL))
|
||||||
.style(
|
.style(
|
||||||
Style::default()
|
Style::default()
|
||||||
@@ -29,42 +33,82 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
);
|
);
|
||||||
f.render_widget(header, message_chunks[0]);
|
f.render_widget(header, message_chunks[0]);
|
||||||
|
|
||||||
// Messages
|
// Messages с группировкой по дате и отправителю
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
let mut last_day: Option<i64> = None;
|
||||||
|
let mut last_sender: Option<(bool, String)> = None; // (is_outgoing, sender_name)
|
||||||
|
|
||||||
for msg in &app.current_messages {
|
for msg in &app.current_messages {
|
||||||
let sender_style = if msg.is_outgoing {
|
// Проверяем, нужно ли добавить разделитель даты
|
||||||
Style::default()
|
let msg_day = get_day(msg.date);
|
||||||
.fg(Color::Green)
|
if last_day != Some(msg_day) {
|
||||||
.add_modifier(Modifier::BOLD)
|
if last_day.is_some() {
|
||||||
} else {
|
lines.push(Line::from("")); // Пустая строка перед разделителем
|
||||||
Style::default()
|
}
|
||||||
.fg(Color::Cyan)
|
// Добавляем разделитель даты
|
||||||
.add_modifier(Modifier::BOLD)
|
let date_str = format_date(msg.date);
|
||||||
};
|
lines.push(Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("──────── {} ────────", date_str),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
last_day = Some(msg_day);
|
||||||
|
last_sender = None; // Сбрасываем отправителя при смене дня
|
||||||
|
}
|
||||||
|
|
||||||
let sender_name = if msg.is_outgoing {
|
let sender_name = if msg.is_outgoing {
|
||||||
"You".to_string()
|
"Вы".to_string()
|
||||||
} else {
|
} else {
|
||||||
msg.sender_name.clone()
|
msg.sender_name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let current_sender = (msg.is_outgoing, sender_name.clone());
|
||||||
|
|
||||||
|
// Проверяем, нужно ли показать заголовок отправителя
|
||||||
|
let show_sender_header = last_sender.as_ref() != Some(¤t_sender);
|
||||||
|
|
||||||
|
if show_sender_header {
|
||||||
|
// Пустая строка между группами сообщений (кроме первой)
|
||||||
|
if last_sender.is_some() {
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
let sender_style = if msg.is_outgoing {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Green)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Заголовок отправителя
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::styled(format!("{} ", sender_name), sender_style),
|
||||||
|
Span::styled("────────────────", Style::default().fg(Color::DarkGray)),
|
||||||
|
]));
|
||||||
|
|
||||||
|
last_sender = Some(current_sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форматируем время (HH:MM)
|
||||||
|
let time = format_timestamp(msg.date);
|
||||||
|
|
||||||
let read_mark = if msg.is_outgoing {
|
let read_mark = if msg.is_outgoing {
|
||||||
if msg.is_read { " ✓✓" } else { " ✓" }
|
if msg.is_read { " ✓✓" } else { " ✓" }
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
// Форматируем время
|
// Сообщение с временем
|
||||||
let time = format_timestamp(msg.date);
|
|
||||||
|
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(format!("{} ", sender_name), sender_style),
|
Span::styled(format!(" [{}]", time), Style::default().fg(Color::DarkGray)),
|
||||||
Span::raw("── "),
|
Span::raw(format!(" {}", msg.content)),
|
||||||
Span::styled(format!("{}{}", time, read_mark), Style::default().fg(Color::DarkGray)),
|
Span::styled(read_mark.to_string(), Style::default().fg(Color::DarkGray)),
|
||||||
]));
|
]));
|
||||||
lines.push(Line::from(msg.content.clone()));
|
|
||||||
lines.push(Line::from(""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if lines.is_empty() {
|
if lines.is_empty() {
|
||||||
|
|||||||
58
src/utils.rs
58
src/utils.rs
@@ -22,26 +22,60 @@ pub fn disable_tdlib_logs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Форматирование timestamp в человекочитаемый формат
|
/// Форматирование timestamp в время HH:MM
|
||||||
pub fn format_timestamp(timestamp: i32) -> String {
|
pub fn format_timestamp(timestamp: i32) -> String {
|
||||||
|
let secs = timestamp as i64;
|
||||||
|
// Конвертируем в локальное время (простой способ без chrono)
|
||||||
|
// UTC + смещение для локального времени
|
||||||
|
let hours = ((secs % 86400) / 3600) as u32;
|
||||||
|
let minutes = ((secs % 3600) / 60) as u32;
|
||||||
|
|
||||||
|
// Примерное локальное время (добавим 3 часа для MSK, можно настроить)
|
||||||
|
let local_hours = (hours + 3) % 24;
|
||||||
|
|
||||||
|
format!("{:02}:{:02}", local_hours, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Форматирование timestamp в дату для разделителя
|
||||||
|
pub fn format_date(timestamp: i32) -> String {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs() as i32;
|
.as_secs() as i64;
|
||||||
|
|
||||||
let diff = now - timestamp;
|
let msg_day = timestamp as i64 / 86400;
|
||||||
|
let today = now / 86400;
|
||||||
|
|
||||||
if diff < 60 {
|
if msg_day == today {
|
||||||
"just now".to_string()
|
"Сегодня".to_string()
|
||||||
} else if diff < 3600 {
|
} else if msg_day == today - 1 {
|
||||||
format!("{}m ago", diff / 60)
|
"Вчера".to_string()
|
||||||
} else if diff < 86400 {
|
|
||||||
format!("{}h ago", diff / 3600)
|
|
||||||
} else {
|
} else {
|
||||||
let secs = timestamp as u64;
|
// Простое форматирование даты
|
||||||
let days = secs / 86400;
|
let days_since_epoch = timestamp as i64 / 86400;
|
||||||
format!("{}d ago", (now as u64 / 86400) - days)
|
// Приблизительный расчёт даты (без учёта високосных годов)
|
||||||
|
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 = 0;
|
||||||
|
let mut day = day_of_year as i32;
|
||||||
|
|
||||||
|
for (i, &m) in months.iter().enumerate() {
|
||||||
|
if day < m {
|
||||||
|
month = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
day -= m;
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{:02}.{:02}.{}", day + 1, month, year)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получить день из timestamp для группировки
|
||||||
|
pub fn get_day(timestamp: i32) -> i64 {
|
||||||
|
timestamp as i64 / 86400
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user