Merge pull request 'add_tests' (#15) from add_tests into main
Some checks failed
CI / Check (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled
Some checks failed
CI / Check (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled
Reviewed-on: #15
This commit is contained in:
@@ -134,11 +134,6 @@ impl ChatState {
|
|||||||
matches!(self, ChatState::PinnedMessages { .. })
|
matches!(self, ChatState::PinnedMessages { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверка: находимся в обычном режиме
|
|
||||||
pub fn is_normal(&self) -> bool {
|
|
||||||
matches!(self, ChatState::Normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Возвращает ID выбранного сообщения (если есть)
|
/// Возвращает ID выбранного сообщения (если есть)
|
||||||
pub fn selected_message_id(&self) -> Option<MessageId> {
|
pub fn selected_message_id(&self) -> Option<MessageId> {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -16,25 +16,6 @@ pub const MAX_CHATS: usize = 200;
|
|||||||
/// Максимальное количество user_ids для хранения в чате
|
/// Максимальное количество user_ids для хранения в чате
|
||||||
pub const MAX_CHAT_USER_IDS: usize = 500;
|
pub const MAX_CHAT_USER_IDS: usize = 500;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// UI Constants
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Количество колонок в emoji picker сетке
|
|
||||||
pub const EMOJI_PICKER_COLUMNS: usize = 8;
|
|
||||||
|
|
||||||
/// Количество рядов в emoji picker сетке
|
|
||||||
pub const EMOJI_PICKER_ROWS: usize = 6;
|
|
||||||
|
|
||||||
/// Максимальная высота поля ввода (в строках)
|
|
||||||
pub const MAX_INPUT_HEIGHT: usize = 10;
|
|
||||||
|
|
||||||
/// Минимальная ширина терминала для корректного отображения
|
|
||||||
pub const MIN_TERMINAL_WIDTH: u16 = 80;
|
|
||||||
|
|
||||||
/// Минимальная высота терминала для корректного отображения
|
|
||||||
pub const MIN_TERMINAL_HEIGHT: u16 = 20;
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Performance
|
// Performance
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -52,18 +33,5 @@ pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
|
|||||||
// TDLib
|
// TDLib
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Лимит количества чатов для загрузки через TDLib за раз
|
|
||||||
pub const TDLIB_CHAT_LIMIT: i32 = 50;
|
|
||||||
|
|
||||||
/// Лимит количества сообщений для загрузки через TDLib за раз
|
/// Лимит количества сообщений для загрузки через TDLib за раз
|
||||||
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
|
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Formatting
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Максимальная длина имени пользователя для отображения
|
|
||||||
pub const MAX_USERNAME_DISPLAY_LENGTH: usize = 20;
|
|
||||||
|
|
||||||
/// Отступ для wrap текста сообщений
|
|
||||||
pub const MESSAGE_TEXT_INDENT: usize = 2;
|
|
||||||
|
|||||||
101
src/error.rs
101
src/error.rs
@@ -1,101 +0,0 @@
|
|||||||
/// Error types for tele-tui application
|
|
||||||
///
|
|
||||||
/// Provides type-safe error handling across the application,
|
|
||||||
/// replacing generic String errors with structured variants.
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum TeletuiError {
|
|
||||||
/// TDLib-related errors
|
|
||||||
#[error("TDLib error: {0}")]
|
|
||||||
TdLib(String),
|
|
||||||
|
|
||||||
/// Configuration errors
|
|
||||||
#[error("Configuration error: {0}")]
|
|
||||||
Config(String),
|
|
||||||
|
|
||||||
/// Network connectivity errors
|
|
||||||
#[error("Network error: {0}")]
|
|
||||||
Network(String),
|
|
||||||
|
|
||||||
/// Authentication errors
|
|
||||||
#[error("Authentication error: {0}")]
|
|
||||||
Auth(String),
|
|
||||||
|
|
||||||
/// Invalid timezone format
|
|
||||||
#[error("Invalid timezone format: {0}")]
|
|
||||||
InvalidTimezone(String),
|
|
||||||
|
|
||||||
/// Invalid color value
|
|
||||||
#[error("Invalid color: {0}")]
|
|
||||||
InvalidColor(String),
|
|
||||||
|
|
||||||
/// Message operation errors
|
|
||||||
#[error("Message error: {0}")]
|
|
||||||
Message(String),
|
|
||||||
|
|
||||||
/// Chat operation errors
|
|
||||||
#[error("Chat error: {0}")]
|
|
||||||
Chat(String),
|
|
||||||
|
|
||||||
/// User operation errors
|
|
||||||
#[error("User error: {0}")]
|
|
||||||
User(String),
|
|
||||||
|
|
||||||
/// File system errors
|
|
||||||
#[error("IO error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
/// TOML parsing errors
|
|
||||||
#[error("TOML error: {0}")]
|
|
||||||
Toml(#[from] toml::de::Error),
|
|
||||||
|
|
||||||
/// JSON parsing errors
|
|
||||||
#[error("JSON error: {0}")]
|
|
||||||
Json(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
/// Clipboard errors
|
|
||||||
#[error("Clipboard error: {0}")]
|
|
||||||
Clipboard(String),
|
|
||||||
|
|
||||||
/// Generic error for cases not covered by specific variants
|
|
||||||
#[error("{0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result type alias using TeletuiError
|
|
||||||
pub type Result<T> = std::result::Result<T, TeletuiError>;
|
|
||||||
|
|
||||||
/// Helper trait for converting String errors to TeletuiError
|
|
||||||
pub trait IntoTeletuiError {
|
|
||||||
fn into_teletui_error(self, variant: ErrorVariant) -> TeletuiError;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoTeletuiError for String {
|
|
||||||
fn into_teletui_error(self, variant: ErrorVariant) -> TeletuiError {
|
|
||||||
match variant {
|
|
||||||
ErrorVariant::TdLib => TeletuiError::TdLib(self),
|
|
||||||
ErrorVariant::Config => TeletuiError::Config(self),
|
|
||||||
ErrorVariant::Network => TeletuiError::Network(self),
|
|
||||||
ErrorVariant::Auth => TeletuiError::Auth(self),
|
|
||||||
ErrorVariant::Message => TeletuiError::Message(self),
|
|
||||||
ErrorVariant::Chat => TeletuiError::Chat(self),
|
|
||||||
ErrorVariant::User => TeletuiError::User(self),
|
|
||||||
ErrorVariant::Clipboard => TeletuiError::Clipboard(self),
|
|
||||||
ErrorVariant::Other => TeletuiError::Other(self),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error variant selector for conversion
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ErrorVariant {
|
|
||||||
TdLib,
|
|
||||||
Config,
|
|
||||||
Network,
|
|
||||||
Auth,
|
|
||||||
Message,
|
|
||||||
Chat,
|
|
||||||
User,
|
|
||||||
Clipboard,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod error;
|
|
||||||
pub mod formatting;
|
pub mod formatting;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod message_grouping;
|
pub mod message_grouping;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
|
||||||
mod formatting;
|
mod formatting;
|
||||||
mod input;
|
mod input;
|
||||||
mod tdlib;
|
mod tdlib;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use tdlib_rs::functions;
|
|||||||
/// Отслеживает текущий этап аутентификации пользователя,
|
/// Отслеживает текущий этап аутентификации пользователя,
|
||||||
/// от инициализации TDLib до полной авторизации.
|
/// от инициализации TDLib до полной авторизации.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum AuthState {
|
pub enum AuthState {
|
||||||
/// Ожидание параметров TDLib (начальное состояние).
|
/// Ожидание параметров TDLib (начальное состояние).
|
||||||
WaitTdlibParameters,
|
WaitTdlibParameters,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use crate::constants::TDLIB_CHAT_LIMIT;
|
|
||||||
use crate::types::{ChatId, UserId};
|
use crate::types::{ChatId, UserId};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tdlib_rs::enums::{ChatAction, ChatList, ChatType};
|
use tdlib_rs::enums::{ChatAction, ChatList, ChatType};
|
||||||
use tdlib_rs::functions;
|
use tdlib_rs::functions;
|
||||||
|
|
||||||
use super::types::{ChatInfo, FolderInfo, MessageInfo, ProfileInfo};
|
use super::types::{ChatInfo, FolderInfo, ProfileInfo};
|
||||||
|
|
||||||
/// Менеджер чатов TDLib.
|
/// Менеджер чатов TDLib.
|
||||||
///
|
///
|
||||||
@@ -183,10 +182,7 @@ impl ChatManager {
|
|||||||
Err(e) => return Err(format!("Ошибка получения чата: {:?}", e)),
|
Err(e) => return Err(format!("Ошибка получения чата: {:?}", e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat = match chat_enum {
|
let tdlib_rs::enums::Chat::Chat(chat) = chat_enum;
|
||||||
tdlib_rs::enums::Chat::Chat(c) => c,
|
|
||||||
_ => return Err("Неожиданный тип чата".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let chat_type_str = match &chat.r#type {
|
let chat_type_str = match &chat.r#type {
|
||||||
ChatType::Private(_) => "Личный чат",
|
ChatType::Private(_) => "Личный чат",
|
||||||
|
|||||||
@@ -59,22 +59,31 @@ pub struct TdClient {
|
|||||||
pub network_state: NetworkState,
|
pub network_state: NetworkState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TdClient {
|
impl TdClient {
|
||||||
/// Creates a new TDLib client instance.
|
/// Creates a new TDLib client instance.
|
||||||
///
|
///
|
||||||
/// Reads API credentials from environment variables `API_ID` and `API_HASH`.
|
/// Reads API credentials from:
|
||||||
|
/// 1. ~/.config/tele-tui/credentials file
|
||||||
|
/// 2. Environment variables `API_ID` and `API_HASH` (fallback)
|
||||||
|
///
|
||||||
/// Initializes all managers and sets initial network state to Connecting.
|
/// Initializes all managers and sets initial network state to Connecting.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A new `TdClient` instance ready for authentication.
|
/// A new `TdClient` instance ready for authentication.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
// Пробуем загрузить credentials из Config (файл или env)
|
||||||
|
let (api_id, api_hash) = crate::config::Config::load_credentials()
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
// Fallback на прямое чтение из env (старое поведение)
|
||||||
let api_id = env::var("API_ID")
|
let api_id = env::var("API_ID")
|
||||||
.unwrap_or_else(|_| "0".to_string())
|
.unwrap_or_else(|_| "0".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let api_hash = env::var("API_HASH").unwrap_or_default();
|
let api_hash = env::var("API_HASH").unwrap_or_default();
|
||||||
|
(api_id, api_hash)
|
||||||
|
});
|
||||||
|
|
||||||
let client_id = tdlib_rs::create_client();
|
let client_id = tdlib_rs::create_client();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -92,15 +101,6 @@ impl TdClient {
|
|||||||
|
|
||||||
// Делегирование к auth
|
// Делегирование к auth
|
||||||
|
|
||||||
/// Checks if the user is authenticated.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// `true` if authentication is complete, `false` otherwise.
|
|
||||||
pub fn is_authenticated(&self) -> bool {
|
|
||||||
self.auth.is_authenticated()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends phone number for authentication.
|
/// Sends phone number for authentication.
|
||||||
///
|
///
|
||||||
/// This is the first step of the authentication flow.
|
/// This is the first step of the authentication flow.
|
||||||
@@ -214,10 +214,6 @@ impl TdClient {
|
|||||||
self.chat_manager.send_chat_action(chat_id, action).await
|
self.chat_manager.send_chat_action(chat_id, action).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_typing_text(&self) -> Option<String> {
|
|
||||||
self.chat_manager.get_typing_text()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_stale_typing_status(&mut self) -> bool {
|
pub fn clear_stale_typing_status(&mut self) -> bool {
|
||||||
self.chat_manager.clear_stale_typing_status()
|
self.chat_manager.clear_stale_typing_status()
|
||||||
}
|
}
|
||||||
@@ -319,10 +315,6 @@ impl TdClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Делегирование к user_cache
|
// Делегирование к user_cache
|
||||||
pub async fn get_user_name(&self, user_id: UserId) -> String {
|
|
||||||
self.user_cache.get_user_name(user_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
pub fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
||||||
self.user_cache.get_status_by_chat_id(chat_id)
|
self.user_cache.get_status_by_chat_id(chat_id)
|
||||||
}
|
}
|
||||||
@@ -361,7 +353,6 @@ impl TdClient {
|
|||||||
pub async fn get_me(&self) -> Result<i64, String> {
|
pub async fn get_me(&self) -> Result<i64, String> {
|
||||||
match functions::get_me(self.client_id).await {
|
match functions::get_me(self.client_id).await {
|
||||||
Ok(tdlib_rs::enums::User::User(user)) => Ok(user.id),
|
Ok(tdlib_rs::enums::User::User(user)) => Ok(user.id),
|
||||||
Ok(_) => Err("Неожиданный тип пользователя".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка получения текущего пользователя: {:?}", e)),
|
Err(e) => Err(format!("Ошибка получения текущего пользователя: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,33 +443,6 @@ impl TdClient {
|
|||||||
&mut self.user_cache
|
&mut self.user_cache
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Инициализация 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
|
|
||||||
self.client_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(format!("Failed to set TDLib parameters: {:?}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Обрабатываем одно обновление от TDLib
|
/// Обрабатываем одно обновление от TDLib
|
||||||
pub fn handle_update(&mut self, update: Update) {
|
pub fn handle_update(&mut self, update: Update) {
|
||||||
match update {
|
match update {
|
||||||
@@ -846,10 +810,7 @@ impl TdClient {
|
|||||||
|
|
||||||
fn add_or_update_chat(&mut self, td_chat_enum: &TdChat) {
|
fn add_or_update_chat(&mut self, td_chat_enum: &TdChat) {
|
||||||
// Pattern match to get inner Chat struct
|
// Pattern match to get inner Chat struct
|
||||||
let td_chat = match td_chat_enum {
|
let TdChat::Chat(td_chat) = td_chat_enum;
|
||||||
TdChat::Chat(chat) => chat,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Пропускаем удалённые аккаунты
|
// Пропускаем удалённые аккаунты
|
||||||
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
|
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
|
||||||
@@ -1102,7 +1063,7 @@ impl TdClient {
|
|||||||
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
|
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
|
||||||
message.forward_info.as_ref().map(|info| {
|
message.forward_info.as_ref().map(|info| {
|
||||||
let sender_name = self.get_origin_sender_name(&info.origin);
|
let sender_name = self.get_origin_sender_name(&info.origin);
|
||||||
ForwardInfo { sender_name, date: info.date }
|
ForwardInfo { sender_name }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::constants::{MAX_MESSAGES_IN_CHAT, TDLIB_MESSAGE_LIMIT};
|
use crate::constants::{MAX_MESSAGES_IN_CHAT, TDLIB_MESSAGE_LIMIT};
|
||||||
use crate::types::{ChatId, MessageId};
|
use crate::types::{ChatId, MessageId};
|
||||||
use tdlib_rs::enums::{ChatAction, InputMessageContent, InputMessageReplyTo, MessageContent, MessageSender, SearchMessagesFilter, TextParseMode};
|
use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, MessageContent, MessageSender, SearchMessagesFilter, TextParseMode};
|
||||||
use tdlib_rs::functions;
|
use tdlib_rs::functions;
|
||||||
use tdlib_rs::types::{Chat as TdChat, FormattedText, InputMessageReplyToMessage, InputMessageText, Message as TdMessage, TextEntity, TextParseModeMarkdown};
|
use tdlib_rs::types::{FormattedText, InputMessageReplyToMessage, InputMessageText, Message as TdMessage, TextParseModeMarkdown};
|
||||||
|
|
||||||
use super::types::{ForwardInfo, MessageBuilder, MessageInfo, ReactionInfo, ReplyInfo};
|
use super::types::{ForwardInfo, MessageBuilder, MessageInfo, ReactionInfo, ReplyInfo};
|
||||||
|
|
||||||
@@ -178,7 +178,6 @@ impl MessageManager {
|
|||||||
sleep(Duration::from_millis(200)).await;
|
sleep(Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(_) => return Err("Неожиданный тип сообщений".to_string()),
|
|
||||||
Err(e) => return Err(format!("Ошибка загрузки истории: {:?}", e)),
|
Err(e) => return Err(format!("Ошибка загрузки истории: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +236,6 @@ impl MessageManager {
|
|||||||
}
|
}
|
||||||
Ok(messages)
|
Ok(messages)
|
||||||
}
|
}
|
||||||
Ok(_) => Err("Неожиданный тип сообщений".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка загрузки старых сообщений: {:?}", e)),
|
Err(e) => Err(format!("Ошибка загрузки старых сообщений: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +284,6 @@ impl MessageManager {
|
|||||||
}
|
}
|
||||||
Ok(pinned_messages)
|
Ok(pinned_messages)
|
||||||
}
|
}
|
||||||
Ok(_) => Err("Неожиданный тип результата поиска".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)),
|
Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,11 +299,10 @@ impl MessageManager {
|
|||||||
/// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`.
|
/// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`.
|
||||||
/// Нужно использовать `getChatPinnedMessage` или альтернативный способ.
|
/// Нужно использовать `getChatPinnedMessage` или альтернативный способ.
|
||||||
/// Временно отключено, возвращает `None`.
|
/// Временно отключено, возвращает `None`.
|
||||||
pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) {
|
pub async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {
|
||||||
// TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat.
|
// TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat.
|
||||||
// Нужно использовать getChatPinnedMessage или альтернативный способ.
|
// Нужно использовать getChatPinnedMessage или альтернативный способ.
|
||||||
// Временно отключено.
|
// Временно отключено.
|
||||||
let _ = chat_id;
|
|
||||||
self.current_pinned_message = None;
|
self.current_pinned_message = None;
|
||||||
|
|
||||||
// match functions::get_chat(chat_id, self.client_id).await {
|
// match functions::get_chat(chat_id, self.client_id).await {
|
||||||
@@ -363,7 +359,6 @@ impl MessageManager {
|
|||||||
}
|
}
|
||||||
Ok(search_results)
|
Ok(search_results)
|
||||||
}
|
}
|
||||||
Ok(_) => Err("Неожиданный тип результата поиска".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка поиска: {:?}", e)),
|
Err(e) => Err(format!("Ошибка поиска: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,7 +463,6 @@ impl MessageManager {
|
|||||||
|
|
||||||
Ok(msg_info)
|
Ok(msg_info)
|
||||||
}
|
}
|
||||||
Ok(_) => Err("Неожиданный тип сообщения".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +518,6 @@ impl MessageManager {
|
|||||||
.convert_message(&msg)
|
.convert_message(&msg)
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| "Не удалось конвертировать отредактированное сообщение".to_string()),
|
.ok_or_else(|| "Не удалось конвертировать отредактированное сообщение".to_string()),
|
||||||
Ok(_) => Err("Неожиданный тип сообщения".to_string()),
|
|
||||||
Err(e) => Err(format!("Ошибка редактирования: {:?}", e)),
|
Err(e) => Err(format!("Ошибка редактирования: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -720,7 +713,6 @@ impl MessageManager {
|
|||||||
if let tdlib_rs::enums::MessageOrigin::User(origin_user) = &fi.origin {
|
if let tdlib_rs::enums::MessageOrigin::User(origin_user) = &fi.origin {
|
||||||
Some(ForwardInfo {
|
Some(ForwardInfo {
|
||||||
sender_name: format!("User {}", origin_user.sender_user_id),
|
sender_name: format!("User {}", origin_user.sender_user_id),
|
||||||
date: fi.date,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -834,7 +826,7 @@ impl MessageManager {
|
|||||||
if let Ok(original_msg_enum) =
|
if let Ok(original_msg_enum) =
|
||||||
functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await
|
functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await
|
||||||
{
|
{
|
||||||
if let tdlib_rs::enums::Message::Message(original_msg) = original_msg_enum {
|
let tdlib_rs::enums::Message::Message(original_msg) = original_msg_enum;
|
||||||
if let Some(orig_info) = self.convert_message(&original_msg).await {
|
if let Some(orig_info) = self.convert_message(&original_msg).await {
|
||||||
// Update the reply info
|
// Update the reply info
|
||||||
for msg in &mut self.current_chat_messages {
|
for msg in &mut self.current_chat_messages {
|
||||||
@@ -856,4 +848,3 @@ impl MessageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ pub mod users;
|
|||||||
pub use auth::AuthState;
|
pub use auth::AuthState;
|
||||||
pub use client::TdClient;
|
pub use client::TdClient;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ChatInfo, FolderInfo, ForwardInfo, MessageBuilder, MessageInfo, NetworkState, ProfileInfo,
|
ChatInfo, MessageBuilder, MessageInfo, NetworkState, ProfileInfo, ReplyInfo, UserOnlineStatus,
|
||||||
ReactionInfo, ReplyInfo, UserOnlineStatus,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export ChatAction для удобства
|
// Re-export ChatAction для удобства
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl ReactionManager {
|
|||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
// Получаем сообщение
|
// Получаем сообщение
|
||||||
let msg_result = functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await;
|
let msg_result = functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await;
|
||||||
let msg = match msg_result {
|
let _msg = match msg_result {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => return Err(format!("Ошибка получения сообщения: {:?}", e)),
|
Err(e) => return Err(format!("Ошибка получения сообщения: {:?}", e)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use tdlib_rs::types::TextEntity;
|
|||||||
use crate::types::{ChatId, MessageId};
|
use crate::types::{ChatId, MessageId};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct ChatInfo {
|
pub struct ChatInfo {
|
||||||
pub id: ChatId,
|
pub id: ChatId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -41,9 +40,6 @@ pub struct ReplyInfo {
|
|||||||
pub struct ForwardInfo {
|
pub struct ForwardInfo {
|
||||||
/// Имя оригинального отправителя
|
/// Имя оригинального отправителя
|
||||||
pub sender_name: String,
|
pub sender_name: String,
|
||||||
/// Дата оригинального сообщения (для будущего использования)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub date: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Информация о реакции на сообщение
|
/// Информация о реакции на сообщение
|
||||||
@@ -164,10 +160,6 @@ impl MessageInfo {
|
|||||||
self.metadata.date
|
self.metadata.date
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_date(&self) -> i32 {
|
|
||||||
self.metadata.edit_date
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_edited(&self) -> bool {
|
pub fn is_edited(&self) -> bool {
|
||||||
self.metadata.edit_date > 0
|
self.metadata.edit_date > 0
|
||||||
}
|
}
|
||||||
@@ -312,12 +304,6 @@ impl MessageBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Пометить сообщение как отредактированное (edit_date = date + 60)
|
|
||||||
pub fn edited(mut self) -> Self {
|
|
||||||
self.edit_date = self.date + 60;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Пометить сообщение как прочитанное
|
/// Пометить сообщение как прочитанное
|
||||||
pub fn read(mut self) -> Self {
|
pub fn read(mut self) -> Self {
|
||||||
self.is_read = true;
|
self.is_read = true;
|
||||||
@@ -366,12 +352,6 @@ impl MessageBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавить одну реакцию
|
|
||||||
pub fn add_reaction(mut self, reaction: ReactionInfo) -> Self {
|
|
||||||
self.reactions.push(reaction);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Построить MessageInfo из данных builder'а
|
/// Построить MessageInfo из данных builder'а
|
||||||
pub fn build(self) -> MessageInfo {
|
pub fn build(self) -> MessageInfo {
|
||||||
MessageInfo::new(
|
MessageInfo::new(
|
||||||
@@ -434,11 +414,11 @@ mod tests {
|
|||||||
let message = MessageBuilder::new(MessageId::new(789))
|
let message = MessageBuilder::new(MessageId::new(789))
|
||||||
.text("Original text")
|
.text("Original text")
|
||||||
.date(1640000000)
|
.date(1640000000)
|
||||||
.edited()
|
.edit_date(1640000060)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert!(message.is_edited());
|
assert!(message.is_edited());
|
||||||
assert_eq!(message.edit_date(), 1640000060);
|
assert_eq!(message.metadata.edit_date, 1640000060);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -468,7 +448,7 @@ mod tests {
|
|||||||
|
|
||||||
let message = MessageBuilder::new(MessageId::new(300))
|
let message = MessageBuilder::new(MessageId::new(300))
|
||||||
.text("Cool message")
|
.text("Cool message")
|
||||||
.add_reaction(reaction.clone())
|
.reactions(vec![reaction.clone()])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(message.reactions().len(), 1);
|
assert_eq!(message.reactions().len(), 1);
|
||||||
|
|||||||
@@ -89,12 +89,6 @@ where
|
|||||||
pub fn contains_key(&self, key: &K) -> bool {
|
pub fn contains_key(&self, key: &K) -> bool {
|
||||||
self.map.contains_key(key)
|
self.map.contains_key(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Количество элементов
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.map.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Кэш информации о пользователях Telegram.
|
/// Кэш информации о пользователях Telegram.
|
||||||
@@ -158,21 +152,6 @@ impl UserCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить username пользователя
|
|
||||||
pub fn get_username(&mut self, user_id: &UserId) -> Option<&String> {
|
|
||||||
self.user_usernames.get(user_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить имя пользователя
|
|
||||||
pub fn get_name(&mut self, user_id: &UserId) -> Option<&String> {
|
|
||||||
self.user_names.get(user_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить user_id по chat_id
|
|
||||||
pub fn get_user_id_by_chat(&self, chat_id: ChatId) -> Option<UserId> {
|
|
||||||
self.chat_user_ids.get(&chat_id).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить статус пользователя по chat_id
|
/// Получить статус пользователя по chat_id
|
||||||
pub fn get_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
pub fn get_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
||||||
let user_id = self.chat_user_ids.get(&chat_id)?;
|
let user_id = self.chat_user_ids.get(&chat_id)?;
|
||||||
@@ -187,7 +166,7 @@ impl UserCache {
|
|||||||
///
|
///
|
||||||
/// * `user_enum` - Обновление пользователя от TDLib
|
/// * `user_enum` - Обновление пользователя от TDLib
|
||||||
pub fn handle_user_update(&mut self, user_enum: &User) {
|
pub fn handle_user_update(&mut self, user_enum: &User) {
|
||||||
if let User::User(user) = user_enum {
|
let User::User(user) = user_enum;
|
||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
|
|
||||||
// Сохраняем username
|
// Сохраняем username
|
||||||
@@ -202,7 +181,6 @@ impl UserCache {
|
|||||||
// Обновляем статус
|
// Обновляем статус
|
||||||
self.update_status(UserId::new(user_id), &user.status);
|
self.update_status(UserId::new(user_id), &user.status);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Обновляет онлайн-статус пользователя.
|
/// Обновляет онлайн-статус пользователя.
|
||||||
///
|
///
|
||||||
@@ -222,11 +200,6 @@ impl UserCache {
|
|||||||
self.user_statuses.insert(user_id, online_status);
|
self.user_statuses.insert(user_id, online_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Сохранить связь chat_id -> user_id
|
|
||||||
pub fn register_private_chat(&mut self, chat_id: ChatId, user_id: UserId) {
|
|
||||||
self.chat_user_ids.insert(chat_id, user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получает имя пользователя из кэша или загружает из TDLib.
|
/// Получает имя пользователя из кэша или загружает из TDLib.
|
||||||
///
|
///
|
||||||
/// Сначала проверяет кэш, затем при необходимости загружает из API.
|
/// Сначала проверяет кэш, затем при необходимости загружает из API.
|
||||||
|
|||||||
@@ -15,12 +15,5 @@
|
|||||||
//
|
//
|
||||||
// Пока этот файл служит placeholder'ом для будущего рефакторинга.
|
// Пока этот файл служит placeholder'ом для будущего рефакторинга.
|
||||||
|
|
||||||
use crate::tdlib::MessageInfo;
|
// Placeholder file - функция render_message_bubble удалена как неиспользуемая.
|
||||||
|
// Рендеринг сообщений находится в src/ui/messages.rs
|
||||||
/// Placeholder для функции рендеринга пузыря сообщения
|
|
||||||
///
|
|
||||||
/// TODO: Реализовать после выполнения P3.8 и P3.9
|
|
||||||
pub fn render_message_bubble(_message: &MessageInfo) {
|
|
||||||
// Будет реализовано позже
|
|
||||||
unimplemented!("Message bubble rendering requires P3.8 and P3.9 first")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ pub mod chat_list_item;
|
|||||||
pub mod emoji_picker;
|
pub mod emoji_picker;
|
||||||
|
|
||||||
// Экспорт основных функций
|
// Экспорт основных функций
|
||||||
pub use modal::render_modal;
|
|
||||||
pub use input_field::render_input_field;
|
pub use input_field::render_input_field;
|
||||||
pub use message_bubble::render_message_bubble;
|
|
||||||
pub use chat_list_item::render_chat_list_item;
|
pub use chat_list_item::render_chat_list_item;
|
||||||
pub use emoji_picker::render_emoji_picker;
|
pub use emoji_picker::render_emoji_picker;
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ fn parse_timezone_offset(tz: &str) -> i32 {
|
|||||||
3 // fallback к MSK
|
3 // fallback к MSK
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Устаревшая функция для обратной совместимости (используется дефолтный +03:00)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn format_timestamp(timestamp: i32) -> String {
|
|
||||||
format_timestamp_with_tz(timestamp, "+03:00")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Форматирование timestamp в дату для разделителя
|
/// Форматирование timestamp в дату для разделителя
|
||||||
pub fn format_date(timestamp: i32) -> String {
|
pub fn format_date(timestamp: i32) -> String {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ fn snapshot_chat_with_online_status() {
|
|||||||
let user_id = tele_tui::types::UserId::new(123);
|
let user_id = tele_tui::types::UserId::new(123);
|
||||||
|
|
||||||
// Регистрируем чат как приватный
|
// Регистрируем чат как приватный
|
||||||
app.td_client.user_cache.register_private_chat(chat_id, user_id);
|
app.td_client.user_cache.chat_user_ids.insert(chat_id, user_id);
|
||||||
|
|
||||||
// Устанавливаем онлайн-статус
|
// Устанавливаем онлайн-статус
|
||||||
app.td_client.user_cache.user_statuses.insert(user_id, UserOnlineStatus::Online);
|
app.td_client.user_cache.user_statuses.insert(user_id, UserOnlineStatus::Online);
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ async fn test_user_journey_edit_during_conversation() {
|
|||||||
let edited_history = client.get_chat_history(ChatId::new(555), 50).await.unwrap();
|
let edited_history = client.get_chat_history(ChatId::new(555), 50).await.unwrap();
|
||||||
assert_eq!(edited_history.len(), 1);
|
assert_eq!(edited_history.len(), 1);
|
||||||
assert_eq!(edited_history[0].text(), "I'll be there at 5pm tomorrow");
|
assert_eq!(edited_history[0].text(), "I'll be there at 5pm tomorrow");
|
||||||
assert!(edited_history[0].edit_date() > 0, "Должна быть установлена дата редактирования");
|
assert!(edited_history[0].metadata.edit_date > 0, "Должна быть установлена дата редактирования");
|
||||||
|
|
||||||
// 6. Проверяем историю редактирований
|
// 6. Проверяем историю редактирований
|
||||||
assert_eq!(client.get_edited_messages().len(), 1);
|
assert_eq!(client.get_edited_messages().len(), 1);
|
||||||
|
|||||||
@@ -39,15 +39,15 @@ async fn test_edit_message_sets_edit_date() {
|
|||||||
// Получаем дату до редактирования
|
// Получаем дату до редактирования
|
||||||
let messages_before = client.get_messages(123);
|
let messages_before = client.get_messages(123);
|
||||||
let date_before = messages_before[0].date();
|
let date_before = messages_before[0].date();
|
||||||
assert_eq!(messages_before[0].edit_date(), 0); // Не редактировалось
|
assert_eq!(messages_before[0].metadata.edit_date, 0); // Не редактировалось
|
||||||
|
|
||||||
// Редактируем сообщение
|
// Редактируем сообщение
|
||||||
client.edit_message(ChatId::new(123), msg.id(), "Edited".to_string()).await.unwrap();
|
client.edit_message(ChatId::new(123), msg.id(), "Edited".to_string()).await.unwrap();
|
||||||
|
|
||||||
// Проверяем что edit_date установлена
|
// Проверяем что edit_date установлена
|
||||||
let messages_after = client.get_messages(123);
|
let messages_after = client.get_messages(123);
|
||||||
assert!(messages_after[0].edit_date() > 0);
|
assert!(messages_after[0].metadata.edit_date > 0);
|
||||||
assert!(messages_after[0].edit_date() > date_before); // edit_date после date
|
assert!(messages_after[0].metadata.edit_date > date_before); // edit_date после date
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: Редактирование только своих сообщений (проверка через can_be_edited)
|
/// Test: Редактирование только своих сообщений (проверка через can_be_edited)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tele_tui::tdlib::{ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo, ReactionInfo};
|
use tele_tui::tdlib::{ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
|
||||||
|
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
|
||||||
use tele_tui::types::{ChatId, MessageId, UserId};
|
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -817,7 +818,7 @@ mod tests {
|
|||||||
let edited = client.get_edited_messages();
|
let edited = client.get_edited_messages();
|
||||||
assert_eq!(edited.len(), 1);
|
assert_eq!(edited.len(), 1);
|
||||||
assert_eq!(client.get_messages(123)[0].text(), "Hello World");
|
assert_eq!(client.get_messages(123)[0].text(), "Hello World");
|
||||||
assert!(client.get_messages(123)[0].edit_date() > 0);
|
assert!(client.get_messages(123)[0].metadata.edit_date > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Test data builders and fixtures
|
// Test data builders and fixtures
|
||||||
|
|
||||||
use tele_tui::tdlib::{ChatInfo, ForwardInfo, MessageInfo, ProfileInfo, ReactionInfo, ReplyInfo};
|
use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo};
|
||||||
|
use tele_tui::tdlib::types::{ForwardInfo, ReactionInfo};
|
||||||
use tele_tui::types::{ChatId, MessageId};
|
use tele_tui::types::{ChatId, MessageId};
|
||||||
|
|
||||||
/// Builder для создания тестового чата
|
/// Builder для создания тестового чата
|
||||||
@@ -176,7 +177,6 @@ impl TestMessageBuilder {
|
|||||||
pub fn forwarded_from(mut self, sender: &str) -> Self {
|
pub fn forwarded_from(mut self, sender: &str) -> Self {
|
||||||
self.forward_from = Some(ForwardInfo {
|
self.forward_from = Some(ForwardInfo {
|
||||||
sender_name: sender.to_string(),
|
sender_name: sender.to_string(),
|
||||||
date: self.date - 3600,
|
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ mod helpers;
|
|||||||
|
|
||||||
use helpers::fake_tdclient::FakeTdClient;
|
use helpers::fake_tdclient::FakeTdClient;
|
||||||
use helpers::test_data::TestMessageBuilder;
|
use helpers::test_data::TestMessageBuilder;
|
||||||
use tele_tui::tdlib::{ForwardInfo, ReplyInfo};
|
use tele_tui::tdlib::ReplyInfo;
|
||||||
|
use tele_tui::tdlib::types::ForwardInfo;
|
||||||
use tele_tui::types::{ChatId, MessageId};
|
use tele_tui::types::{ChatId, MessageId};
|
||||||
|
|
||||||
/// Test: Reply создаёт сообщение с reply_to
|
/// Test: Reply создаёт сообщение с reply_to
|
||||||
@@ -106,7 +107,6 @@ async fn test_forward_creates_message_with_forward_from() {
|
|||||||
|
|
||||||
let forward = messages[0].forward_from().unwrap();
|
let forward = messages[0].forward_from().unwrap();
|
||||||
assert_eq!(forward.sender_name, "Bob");
|
assert_eq!(forward.sender_name, "Bob");
|
||||||
assert!(forward.date > 0); // Дата установлена
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: Forward показывает "↪ Переслано от ..."
|
/// Test: Forward показывает "↪ Переслано от ..."
|
||||||
|
|||||||
Reference in New Issue
Block a user