refactor: use system timezone and harden client APIs #29
@@ -1,12 +1,12 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use tele_tui::utils::formatting::{format_date, format_timestamp_with_tz, get_day};
|
||||
use tele_tui::utils::formatting::{format_date, format_timestamp, get_day};
|
||||
|
||||
fn benchmark_format_timestamp(c: &mut Criterion) {
|
||||
c.bench_function("format_timestamp_50_times", |b| {
|
||||
b.iter(|| {
|
||||
for i in 0..50 {
|
||||
let timestamp = 1640000000 + (i * 60);
|
||||
black_box(format_timestamp_with_tz(timestamp, "+03:00"));
|
||||
black_box(format_timestamp(timestamp));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
# Этот файл автоматически создаётся при первом запуске в ~/.config/tele-tui/config.toml
|
||||
# Скопируйте его туда и настройте по своему усмотрению
|
||||
|
||||
[general]
|
||||
# Часовой пояс в формате "+03:00" или "-05:00"
|
||||
# Применяется к отображению времени сообщений
|
||||
timezone = "+03:00"
|
||||
|
||||
[colors]
|
||||
# Цветовая схема интерфейса
|
||||
# Поддерживаемые цвета:
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
| `e` | `у` | Открыть emoji picker |
|
||||
| `v` | `м` | Открыть фото в модалке |
|
||||
| `Space` | | Play / pause голосового сообщения |
|
||||
| `Ctrl+I` | `Ctrl+Ш` | Профиль чата или пользователя |
|
||||
| `Tab` / `Ctrl+I` | `Ctrl+Ш` | Профиль чата или пользователя |
|
||||
|
||||
## Модалки
|
||||
|
||||
|
||||
@@ -613,6 +613,8 @@ impl App<TdClient> {
|
||||
///
|
||||
/// A new `App<TdClient>` instance ready to start authentication.
|
||||
pub fn new(config: crate::config::Config, db_path: std::path::PathBuf) -> App<TdClient> {
|
||||
App::with_client(config, TdClient::new(db_path))
|
||||
let mut td_client = TdClient::new(db_path);
|
||||
td_client.configure_notifications(&config.notifications);
|
||||
App::with_client(config, td_client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,8 @@ impl Default for Keybindings {
|
||||
bindings.insert(
|
||||
Command::OpenProfile,
|
||||
vec![
|
||||
// Во многих терминалах Ctrl+I приходит как Tab
|
||||
KeyBinding::new(KeyCode::Tab),
|
||||
KeyBinding::with_ctrl(KeyCode::Char('i')),
|
||||
KeyBinding::with_ctrl(KeyCode::Char('ш')), // RU
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Configuration module.
|
||||
//!
|
||||
//! Loads settings from `~/.config/tele-tui/config.toml`.
|
||||
//! Structs: Config, GeneralConfig, ColorsConfig, NotificationsConfig, Keybindings.
|
||||
//! Structs: Config, ColorsConfig, NotificationsConfig, Keybindings.
|
||||
|
||||
pub mod keybindings;
|
||||
mod loader;
|
||||
@@ -23,15 +23,10 @@ pub use keybindings::{Command, Keybindings};
|
||||
/// let config = Config::load();
|
||||
///
|
||||
/// // Доступ к настройкам
|
||||
/// println!("Timezone: {}", config.general.timezone);
|
||||
/// println!("Incoming color: {}", config.colors.incoming_message);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Общие настройки (timezone и т.д.).
|
||||
#[serde(default)]
|
||||
pub general: GeneralConfig,
|
||||
|
||||
/// Цветовая схема интерфейса.
|
||||
#[serde(default)]
|
||||
pub colors: ColorsConfig,
|
||||
@@ -53,14 +48,6 @@ pub struct Config {
|
||||
pub audio: AudioConfig,
|
||||
}
|
||||
|
||||
/// Общие настройки приложения.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneralConfig {
|
||||
/// Часовой пояс в формате "+03:00" или "-05:00"
|
||||
#[serde(default = "default_timezone")]
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
/// Цветовая схема интерфейса.
|
||||
///
|
||||
/// Поддерживаемые цвета: red, green, blue, yellow, cyan, magenta,
|
||||
@@ -166,10 +153,6 @@ impl Default for AudioConfig {
|
||||
}
|
||||
|
||||
// Дефолтные значения (используются serde атрибутами)
|
||||
fn default_timezone() -> String {
|
||||
"+03:00".to_string()
|
||||
}
|
||||
|
||||
fn default_incoming_color() -> String {
|
||||
"white".to_string()
|
||||
}
|
||||
@@ -230,12 +213,6 @@ fn default_auto_download_voice() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self { timezone: default_timezone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -295,32 +272,6 @@ mod tests {
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validate_invalid_timezone_no_sign() {
|
||||
let mut config = Config::default();
|
||||
config.general.timezone = "03:00".to_string();
|
||||
|
||||
let result = config.validate();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("timezone"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validate_valid_negative_timezone() {
|
||||
let mut config = Config::default();
|
||||
config.general.timezone = "-05:00".to_string();
|
||||
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validate_valid_positive_timezone() {
|
||||
let mut config = Config::default();
|
||||
config.general.timezone = "+09:00".to_string();
|
||||
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validate_invalid_color_incoming() {
|
||||
let mut config = Config::default();
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
//! Config validation: timezone format, color names, notification settings.
|
||||
//! Config validation: color names, notification settings.
|
||||
|
||||
use super::Config;
|
||||
|
||||
impl Config {
|
||||
/// Валидация конфигурации
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// Проверка timezone
|
||||
if !self.general.timezone.starts_with('+') && !self.general.timezone.starts_with('-') {
|
||||
return Err(format!(
|
||||
"Invalid timezone (must start with + or -): {}",
|
||||
self.general.timezone
|
||||
));
|
||||
}
|
||||
|
||||
// Проверка цветов
|
||||
let valid_colors = [
|
||||
"black",
|
||||
|
||||
@@ -47,8 +47,7 @@ pub async fn open_chat_and_load_data<T: TdClientTrait>(app: &mut App<T>, chat_id
|
||||
// Добавляем входящие сообщения в очередь для отметки как прочитанные
|
||||
if !incoming_message_ids.is_empty() {
|
||||
app.td_client
|
||||
.pending_view_messages_mut()
|
||||
.push((ChatId::new(chat_id), incoming_message_ids));
|
||||
.enqueue_pending_view_messages(ChatId::new(chat_id), incoming_message_ids);
|
||||
}
|
||||
|
||||
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
|
||||
|
||||
@@ -434,6 +434,8 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
|
||||
app.error_message = Some(format!("Ошибка переключения: {}", e));
|
||||
continue;
|
||||
}
|
||||
let notifications_cfg = app.config().notifications.clone();
|
||||
app.td_client.configure_notifications(¬ifications_cfg);
|
||||
|
||||
// 4. Reset app state
|
||||
app.current_account_name = account_name.clone();
|
||||
|
||||
@@ -63,7 +63,7 @@ pub enum MessageGroup<'a> {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup<'_>> {
|
||||
pub fn group_messages<'a>(messages: &'a [MessageInfo]) -> Vec<MessageGroup<'a>> {
|
||||
let mut result = Vec::new();
|
||||
let mut last_day: Option<i64> = None;
|
||||
let mut last_sender: Option<(bool, String)> = None; // (is_outgoing, sender_name)
|
||||
@@ -257,14 +257,14 @@ mod tests {
|
||||
assert_eq!(grouped.len(), 5);
|
||||
|
||||
if let MessageGroup::SenderHeader { is_outgoing, sender_name } = &grouped[1] {
|
||||
assert_eq!(*is_outgoing, false);
|
||||
assert!(!*is_outgoing);
|
||||
assert_eq!(sender_name, "Alice");
|
||||
} else {
|
||||
panic!("Expected SenderHeader");
|
||||
}
|
||||
|
||||
if let MessageGroup::SenderHeader { is_outgoing, sender_name } = &grouped[3] {
|
||||
assert_eq!(*is_outgoing, true);
|
||||
assert!(*is_outgoing);
|
||||
assert_eq!(sender_name, "Вы");
|
||||
} else {
|
||||
panic!("Expected SenderHeader");
|
||||
|
||||
@@ -67,6 +67,11 @@ impl NotificationManager {
|
||||
self.timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
/// Sets whether message preview text should be shown in notifications
|
||||
pub fn set_show_preview(&mut self, show_preview: bool) {
|
||||
self.show_preview = show_preview;
|
||||
}
|
||||
|
||||
/// Sets notification urgency level
|
||||
pub fn set_urgency(&mut self, urgency: String) {
|
||||
self.urgency = urgency;
|
||||
|
||||
@@ -105,10 +105,10 @@ impl TdClient {
|
||||
self.notification_manager.set_enabled(config.enabled);
|
||||
self.notification_manager
|
||||
.set_only_mentions(config.only_mentions);
|
||||
self.notification_manager.set_show_preview(config.show_preview);
|
||||
self.notification_manager.set_timeout(config.timeout_ms);
|
||||
self.notification_manager
|
||||
.set_urgency(config.urgency.clone());
|
||||
// Note: show_preview is used when formatting notification body
|
||||
}
|
||||
|
||||
/// Synchronizes muted chats from Telegram to notification manager.
|
||||
@@ -484,10 +484,14 @@ impl TdClient {
|
||||
&self.message_manager.pending_view_messages
|
||||
}
|
||||
|
||||
pub fn pending_view_messages_mut(
|
||||
pub fn enqueue_pending_view_messages(
|
||||
&mut self,
|
||||
) -> &mut Vec<(crate::types::ChatId, Vec<crate::types::MessageId>)> {
|
||||
&mut self.message_manager.pending_view_messages
|
||||
chat_id: crate::types::ChatId,
|
||||
message_ids: Vec<crate::types::MessageId>,
|
||||
) {
|
||||
self.message_manager
|
||||
.pending_view_messages
|
||||
.push((chat_id, message_ids));
|
||||
}
|
||||
|
||||
pub fn pending_user_ids(&self) -> &[crate::types::UserId] {
|
||||
|
||||
@@ -270,8 +270,8 @@ impl TdClientTrait for TdClient {
|
||||
self.set_typing_status(status)
|
||||
}
|
||||
|
||||
fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec<MessageId>)> {
|
||||
self.pending_view_messages_mut()
|
||||
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||||
self.enqueue_pending_view_messages(chat_id, message_ids);
|
||||
}
|
||||
|
||||
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId> {
|
||||
@@ -287,6 +287,10 @@ impl TdClientTrait for TdClient {
|
||||
}
|
||||
|
||||
// ============ Notification methods ============
|
||||
fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) {
|
||||
self.configure_notifications(config);
|
||||
}
|
||||
|
||||
fn sync_notification_muted_chats(&mut self) {
|
||||
self.notification_manager
|
||||
.sync_muted_chats(&self.chat_manager.chats);
|
||||
|
||||
@@ -133,12 +133,13 @@ pub trait TdClientTrait: Send {
|
||||
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>);
|
||||
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>);
|
||||
fn set_typing_status(&mut self, status: Option<(UserId, String, std::time::Instant)>);
|
||||
fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec<MessageId>)>;
|
||||
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>);
|
||||
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId>;
|
||||
fn set_main_chat_list_position(&mut self, position: i32);
|
||||
fn user_cache_mut(&mut self) -> &mut UserCache;
|
||||
|
||||
// ============ Notification methods ============
|
||||
fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig);
|
||||
fn sync_notification_muted_chats(&mut self);
|
||||
|
||||
// ============ Account switching ============
|
||||
|
||||
@@ -72,9 +72,7 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag
|
||||
client.push_message(msg_info.clone());
|
||||
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
|
||||
if is_incoming {
|
||||
client
|
||||
.pending_view_messages_mut()
|
||||
.push((chat_id, vec![msg_id]));
|
||||
client.enqueue_pending_view_messages(chat_id, vec![msg_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::formatting;
|
||||
use crate::tdlib::PhotoDownloadState;
|
||||
use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus};
|
||||
use crate::types::MessageId;
|
||||
use crate::utils::{format_date, format_timestamp_with_tz};
|
||||
use crate::utils::{format_date, format_timestamp};
|
||||
use ratatui::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
@@ -208,7 +208,7 @@ pub fn render_sender_header(
|
||||
/// # Аргументы
|
||||
///
|
||||
/// * `msg` - сообщение для рендеринга
|
||||
/// * `config` - конфигурация (цвета, timezone)
|
||||
/// * `config` - конфигурация (цвета)
|
||||
/// * `content_width` - ширина области для рендеринга
|
||||
/// * `selected_msg_id` - ID выбранного сообщения (для подсветки)
|
||||
pub fn render_message_bubble(
|
||||
@@ -277,7 +277,7 @@ pub fn render_message_bubble(
|
||||
}
|
||||
|
||||
// Форматируем время
|
||||
let time = format_timestamp_with_tz(msg.date(), &config.general.timezone);
|
||||
let time = format_timestamp(msg.date());
|
||||
|
||||
if msg.is_outgoing() {
|
||||
// Исходящие: справа, формат "текст (HH:MM ✎ ✓✓)"
|
||||
@@ -671,7 +671,7 @@ pub fn render_album_bubble(
|
||||
|
||||
// Timestamp из последнего сообщения
|
||||
let last_msg = messages.last().unwrap();
|
||||
let time = format_timestamp_with_tz(last_msg.date(), &config.general.timezone);
|
||||
let time = format_timestamp(last_msg.date());
|
||||
|
||||
if !captions.is_empty() {
|
||||
let caption_text = captions.join(" ");
|
||||
|
||||
@@ -196,7 +196,11 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
|
||||
match group {
|
||||
MessageGroup::DateSeparator(date) => {
|
||||
// Рендерим разделитель даты
|
||||
lines.extend(components::render_date_separator(date, content_width, is_first_date));
|
||||
lines.extend(components::render_date_separator(
|
||||
date,
|
||||
content_width,
|
||||
is_first_date,
|
||||
));
|
||||
is_first_date = false;
|
||||
is_first_sender = true; // Сбрасываем счётчик заголовков после даты
|
||||
}
|
||||
|
||||
@@ -56,7 +56,12 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
if idx > 0 {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
lines.extend(render_message_item(msg, idx == selected_index, content_width, 3));
|
||||
lines.extend(render_message_item(
|
||||
msg,
|
||||
idx == selected_index,
|
||||
content_width,
|
||||
3,
|
||||
));
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
|
||||
@@ -80,7 +80,12 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
if idx > 0 {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
lines.extend(render_message_item(msg, idx == selected_index, content_width, 2));
|
||||
lines.extend(render_message_item(
|
||||
msg,
|
||||
idx == selected_index,
|
||||
content_width,
|
||||
2,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +1,49 @@
|
||||
/// Форматирование timestamp в время HH:MM с учётом timezone offset
|
||||
/// timezone_str: строка формата "+03:00" или "-05:00"
|
||||
pub fn format_timestamp_with_tz(timestamp: i32, timezone_str: &str) -> String {
|
||||
let secs = timestamp as i64;
|
||||
use chrono::{DateTime, Local, NaiveDate, Utc};
|
||||
|
||||
// Парсим timezone offset (например "+03:00" -> 3 часа)
|
||||
let offset_hours = parse_timezone_offset(timezone_str);
|
||||
|
||||
let hours = ((secs % 86400) / 3600) as i32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
|
||||
// Применяем timezone offset
|
||||
let local_hours = ((hours + offset_hours) % 24 + 24) % 24;
|
||||
|
||||
format!("{:02}:{:02}", local_hours, minutes)
|
||||
fn as_local_datetime(timestamp: i32) -> Option<DateTime<Local>> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp as i64, 0).map(|dt| dt.with_timezone(&Local))
|
||||
}
|
||||
|
||||
/// Парсит timezone строку типа "+03:00" в количество часов
|
||||
fn parse_timezone_offset(tz: &str) -> i32 {
|
||||
// Простой парсинг "+03:00" или "-05:00"
|
||||
if tz.len() >= 3 {
|
||||
let sign = if tz.starts_with('-') { -1 } else { 1 };
|
||||
let hours_str = &tz[1..3];
|
||||
if let Ok(hours) = hours_str.parse::<i32>() {
|
||||
return sign * hours;
|
||||
}
|
||||
}
|
||||
3 // fallback к MSK
|
||||
/// Форматирование timestamp во время HH:MM в системной таймзоне.
|
||||
pub fn format_timestamp(timestamp: i32) -> String {
|
||||
as_local_datetime(timestamp)
|
||||
.map(|dt| dt.format("%H:%M").to_string())
|
||||
.unwrap_or_else(|| "00:00".to_string())
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в дату для разделителя
|
||||
/// Форматирование timestamp в дату для разделителя.
|
||||
pub fn format_date(timestamp: i32) -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let Some(msg_dt) = as_local_datetime(timestamp) else {
|
||||
return "01.01.1970".to_string();
|
||||
};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let msg_day = timestamp as i64 / 86400;
|
||||
let today = now / 86400;
|
||||
let msg_day = msg_dt.date_naive();
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
if msg_day == today {
|
||||
"Сегодня".to_string()
|
||||
} else if msg_day == today - 1 {
|
||||
} else if Some(msg_day) == today.pred_opt() {
|
||||
"Вчера".to_string()
|
||||
} else {
|
||||
// Простое форматирование даты
|
||||
let days_since_epoch = timestamp as i64 / 86400;
|
||||
// Приблизительный расчёт даты (без учёта високосных годов)
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 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)
|
||||
msg_dt.format("%d.%m.%Y").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить день из timestamp для группировки
|
||||
/// Получить день из timestamp для группировки.
|
||||
/// Возвращает число дней с 1970-01-01 в системной таймзоне.
|
||||
pub fn get_day(timestamp: i32) -> i64 {
|
||||
timestamp as i64 / 86400
|
||||
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch date");
|
||||
|
||||
as_local_datetime(timestamp)
|
||||
.map(|dt| dt.date_naive().signed_duration_since(epoch).num_days())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM) в системной таймзоне.
|
||||
pub fn format_datetime(timestamp: i32) -> String {
|
||||
let secs = timestamp as i64;
|
||||
|
||||
// Время
|
||||
let hours = ((secs % 86400) / 3600) as u32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
let local_hours = (hours + 3) % 24; // MSK
|
||||
|
||||
// Дата
|
||||
let days_since_epoch = secs / 86400;
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 1;
|
||||
let mut day = day_of_year as i32;
|
||||
|
||||
for (i, &m) in months.iter().enumerate() {
|
||||
if day < m {
|
||||
month = i + 1;
|
||||
break;
|
||||
}
|
||||
day -= m;
|
||||
}
|
||||
|
||||
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
|
||||
as_local_datetime(timestamp)
|
||||
.map(|dt| dt.format("%d.%m.%Y %H:%M").to_string())
|
||||
.unwrap_or_else(|| "01.01.1970 00:00".to_string())
|
||||
}
|
||||
|
||||
/// Форматирование "был(а) онлайн" из timestamp
|
||||
@@ -121,8 +66,8 @@ pub fn format_was_online(timestamp: i32) -> String {
|
||||
let hours = diff / 3600;
|
||||
format!("был(а) {} ч. назад", hours)
|
||||
} else {
|
||||
// Показываем дату
|
||||
let datetime = chrono::DateTime::from_timestamp(timestamp as i64, 0)
|
||||
// Показываем локальную дату
|
||||
let datetime = as_local_datetime(timestamp)
|
||||
.map(|dt| dt.format("%d.%m %H:%M").to_string())
|
||||
.unwrap_or_else(|| "давно".to_string());
|
||||
format!("был(а) {}", datetime)
|
||||
@@ -134,117 +79,49 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_positive_offset() {
|
||||
// 2021-12-20 11:33:20 UTC (1640000000)
|
||||
fn test_format_timestamp_matches_local_timezone() {
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// +03:00 должно дать 14:33 (11 + 3)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+03:00"), "14:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_negative_offset() {
|
||||
// 2021-12-20 11:33:20 UTC
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// -05:00 должно дать 06:33 (11 - 5)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "-05:00"), "06:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_zero_offset() {
|
||||
// 2021-12-20 11:33:20 UTC
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// +00:00 должно дать UTC время 11:33
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+00:00"), "11:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_midnight_wrap() {
|
||||
// Тест перехода через полночь
|
||||
let timestamp = 82800; // 23:00 UTC (первый день эпохи)
|
||||
|
||||
// +02:00 должно дать 01:00 (следующего дня)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+02:00"), "01:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_invalid_fallback() {
|
||||
let timestamp = 1640000000; // 11:33:20 UTC
|
||||
|
||||
// Невалидный timezone должен использовать fallback +03:00 -> 14:33
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "invalid"), "14:33");
|
||||
let expected = as_local_datetime(timestamp)
|
||||
.unwrap()
|
||||
.format("%H:%M")
|
||||
.to_string();
|
||||
assert_eq!(format_timestamp(timestamp), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day() {
|
||||
// Первый день эпохи (1970-01-01)
|
||||
assert_eq!(get_day(0), 0);
|
||||
|
||||
// Второй день (1970-01-02)
|
||||
assert_eq!(get_day(86400), 1);
|
||||
|
||||
// Конкретная дата: 2021-12-20 (18976 дней после эпохи)
|
||||
assert_eq!(get_day(1640000000), 18981);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day_grouping() {
|
||||
// Сообщения в один день должны иметь одинаковый day
|
||||
let msg1 = 1640000000; // 2021-12-20 09:33:20
|
||||
let msg2 = 1640040000; // 2021-12-20 20:40:00
|
||||
|
||||
let msg1 = 1640000000;
|
||||
let msg2 = msg1 + 3600;
|
||||
assert_eq!(get_day(msg1), get_day(msg2));
|
||||
|
||||
// Сообщения в разные дни должны различаться
|
||||
let msg3 = 1640100000; // 2021-12-21 13:26:40
|
||||
|
||||
let msg3 = msg1 + 172800;
|
||||
assert_ne!(get_day(msg1), get_day(msg3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_datetime() {
|
||||
// 2021-12-20 11:33:20 UTC -> с MSK (+03:00) = 14:33:20
|
||||
let timestamp = 1640000000;
|
||||
let result = format_datetime(timestamp);
|
||||
|
||||
// Проверяем что результат содержит время с MSK offset
|
||||
assert!(result.contains("14:33"), "Expected '14:33' in '{}'", result);
|
||||
// Проверяем формат (должен быть DD.MM.YYYY HH:MM)
|
||||
assert_eq!(result.chars().filter(|&c| c == '.').count(), 2);
|
||||
assert!(result.contains(":"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timezone_offset_via_format() {
|
||||
// Тестируем parse_timezone_offset через публичную функцию
|
||||
let base_timestamp = 0; // 00:00:00 UTC
|
||||
|
||||
// +03:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "+03:00"), "03:00");
|
||||
|
||||
// -05:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "-05:00"), "19:00");
|
||||
|
||||
// +12:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "+12:00"), "12:00");
|
||||
|
||||
// -11:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "-11:00"), "13:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_today() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Получаем текущий timestamp
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Сообщение от сегодня
|
||||
let result = format_date(now);
|
||||
assert_eq!(result, "Сегодня");
|
||||
}
|
||||
@@ -258,7 +135,6 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Вчера = now - 1 день (86400 секунд)
|
||||
let yesterday = now - 86400;
|
||||
let result = format_date(yesterday);
|
||||
assert_eq!(result, "Вчера");
|
||||
@@ -266,25 +142,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_format_date_old() {
|
||||
// Старая дата: 2021-12-20 (timestamp 1640000000)
|
||||
let old_timestamp = 1640000000;
|
||||
let result = format_date(old_timestamp);
|
||||
|
||||
// Должен быть формат DD.MM.YYYY
|
||||
assert!(result.contains('.'), "Expected date format with dots");
|
||||
assert_ne!(result, "Сегодня");
|
||||
assert_ne!(result, "Вчера");
|
||||
// Проверяем что есть три части (день.месяц.год)
|
||||
assert_eq!(result.split('.').count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_epoch() {
|
||||
// Начало эпохи: 1970-01-01
|
||||
let epoch = 0;
|
||||
let result = format_date(epoch);
|
||||
|
||||
// Должен быть формат даты (не "Сегодня" или "Вчера")
|
||||
assert!(result.contains('.'));
|
||||
assert!(result.contains("1970"));
|
||||
}
|
||||
@@ -298,7 +169,6 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Был онлайн только что (30 секунд назад)
|
||||
let recent = now - 30;
|
||||
let result = format_was_online(recent);
|
||||
assert_eq!(result, "был(а) только что");
|
||||
@@ -313,7 +183,6 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Был онлайн 15 минут назад
|
||||
let mins_ago = now - (15 * 60);
|
||||
let result = format_was_online(mins_ago);
|
||||
assert_eq!(result, "был(а) 15 мин. назад");
|
||||
@@ -328,7 +197,6 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Был онлайн 5 часов назад
|
||||
let hours_ago = now - (5 * 3600);
|
||||
let result = format_was_online(hours_ago);
|
||||
assert_eq!(result, "был(а) 5 ч. назад");
|
||||
@@ -343,22 +211,18 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
// Был онлайн 3 дня назад
|
||||
let days_ago = now - (3 * 86400);
|
||||
let result = format_was_online(days_ago);
|
||||
|
||||
// Должен содержать "был(а)" и дату
|
||||
assert!(result.starts_with("был(а)"));
|
||||
assert!(result.contains('.') || result.contains(':'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_very_old() {
|
||||
// Очень старый timestamp (2020-01-01)
|
||||
let old = 1577836800;
|
||||
let result = format_was_online(old);
|
||||
|
||||
// Должен содержать "был(а)" и дату
|
||||
assert!(result.starts_with("был(а)"));
|
||||
assert!(result.contains('.'));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Integration tests for config flow
|
||||
|
||||
use tele_tui::config::{
|
||||
AudioConfig, ColorsConfig, Config, GeneralConfig, ImagesConfig, Keybindings,
|
||||
NotificationsConfig,
|
||||
AudioConfig, ColorsConfig, Config, ImagesConfig, Keybindings, NotificationsConfig,
|
||||
};
|
||||
|
||||
/// Test: Дефолтные значения конфигурации
|
||||
@@ -10,9 +9,6 @@ use tele_tui::config::{
|
||||
fn test_config_default_values() {
|
||||
let config = Config::default();
|
||||
|
||||
// Проверяем дефолтный timezone
|
||||
assert_eq!(config.general.timezone, "+03:00");
|
||||
|
||||
// Проверяем дефолтные цвета
|
||||
assert_eq!(config.colors.incoming_message, "white");
|
||||
assert_eq!(config.colors.outgoing_message, "green");
|
||||
@@ -25,7 +21,6 @@ fn test_config_default_values() {
|
||||
#[test]
|
||||
fn test_config_custom_values() {
|
||||
let config = Config {
|
||||
general: GeneralConfig { timezone: "+05:00".to_string() },
|
||||
colors: ColorsConfig {
|
||||
incoming_message: "cyan".to_string(),
|
||||
outgoing_message: "blue".to_string(),
|
||||
@@ -39,7 +34,6 @@ fn test_config_custom_values() {
|
||||
audio: AudioConfig::default(),
|
||||
};
|
||||
|
||||
assert_eq!(config.general.timezone, "+05:00");
|
||||
assert_eq!(config.colors.incoming_message, "cyan");
|
||||
assert_eq!(config.colors.outgoing_message, "blue");
|
||||
}
|
||||
@@ -109,7 +103,6 @@ fn test_parse_color_case_insensitive() {
|
||||
#[test]
|
||||
fn test_config_toml_serialization() {
|
||||
let original_config = Config {
|
||||
general: GeneralConfig { timezone: "-05:00".to_string() },
|
||||
colors: ColorsConfig {
|
||||
incoming_message: "cyan".to_string(),
|
||||
outgoing_message: "blue".to_string(),
|
||||
@@ -130,7 +123,6 @@ fn test_config_toml_serialization() {
|
||||
let deserialized: Config = toml::from_str(&toml_string).expect("Failed to deserialize config");
|
||||
|
||||
// Проверяем что всё совпадает
|
||||
assert_eq!(deserialized.general.timezone, "-05:00");
|
||||
assert_eq!(deserialized.colors.incoming_message, "cyan");
|
||||
assert_eq!(deserialized.colors.outgoing_message, "blue");
|
||||
assert_eq!(deserialized.colors.selected_message, "red");
|
||||
@@ -139,47 +131,19 @@ fn test_config_toml_serialization() {
|
||||
/// Test: Парсинг TOML с частичными данными использует дефолты
|
||||
#[test]
|
||||
fn test_config_partial_toml_uses_defaults() {
|
||||
// TOML только с timezone, без colors
|
||||
// TOML только с colors.incoming_message
|
||||
let toml_str = r#"
|
||||
[general]
|
||||
timezone = "+02:00"
|
||||
[colors]
|
||||
incoming_message = "cyan"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml_str).expect("Failed to parse partial TOML");
|
||||
|
||||
// Timezone должен быть из TOML
|
||||
assert_eq!(config.general.timezone, "+02:00");
|
||||
|
||||
// Colors должны быть дефолтными
|
||||
assert_eq!(config.colors.incoming_message, "white");
|
||||
// Кастомный цвет должен примениться
|
||||
assert_eq!(config.colors.incoming_message, "cyan");
|
||||
// Остальные colors должны быть дефолтными
|
||||
assert_eq!(config.colors.outgoing_message, "green");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod timezone_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test: Различные форматы timezone
|
||||
#[test]
|
||||
fn test_timezone_formats() {
|
||||
let positive = Config {
|
||||
general: GeneralConfig { timezone: "+03:00".to_string() },
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(positive.general.timezone, "+03:00");
|
||||
|
||||
let negative = Config {
|
||||
general: GeneralConfig { timezone: "-05:00".to_string() },
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(negative.general.timezone, "-05:00");
|
||||
|
||||
let zero = Config {
|
||||
general: GeneralConfig { timezone: "+00:00".to_string() },
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(zero.general.timezone, "+00:00");
|
||||
}
|
||||
assert_eq!(config.colors.selected_message, "yellow");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -313,11 +313,11 @@ impl TdClientTrait for FakeTdClient {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec<MessageId>)> {
|
||||
// WORKAROUND: Возвращаем мутабельную ссылку через leak
|
||||
// Это безопасно так как мы единственные владельцы &mut self
|
||||
let guard = self.pending_view_messages.lock().unwrap();
|
||||
unsafe { &mut *(guard.as_ptr() as *mut Vec<(ChatId, Vec<MessageId>)>) }
|
||||
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||||
self.pending_view_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id, message_ids));
|
||||
}
|
||||
|
||||
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId> {
|
||||
@@ -333,6 +333,10 @@ impl TdClientTrait for FakeTdClient {
|
||||
}
|
||||
|
||||
// ============ Notification methods ============
|
||||
fn configure_notifications(&mut self, _config: &tele_tui::config::NotificationsConfig) {
|
||||
// Not implemented for fake client (notifications are not tested)
|
||||
}
|
||||
|
||||
fn sync_notification_muted_chats(&mut self) {
|
||||
// Not implemented for fake client (notifications are not tested)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
assertion_line: 111
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ ▶ Original message text (14:33 ✓✓) │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
assertion_line: 135
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Mom ──────────────── │
|
||||
│ (14:33) What do you think about this? │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 418
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 444
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ 📷 [Фото] (14:33 ✓✓)│
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 503
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 476
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Group Chat │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) Regular message before │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 87
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Message from the past │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 186
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33 ✎) Edited text │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 325
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│↪ Переслано от Alice │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 206
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) This is a very long message that should wrap across multiple lines │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 225
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) **bold** *italic* `code` │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 245
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Check [this](https://example.com) and @username │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 264
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Spoiler: ||hidden text|| │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 283
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) [Фото] │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 368
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Popular message │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 167
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Read message (14:33 ✓✓) │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 137
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Just sent (14:33 ✓✓) │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 304
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│┌ Mom: Original message text │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 388
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Selected message │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 118
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Group Chat │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) First message │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 46
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Mom ──────────────── │
|
||||
│ (14:33) Hello there! │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 65
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Hi mom! (14:33 ✓✓) │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 346
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Great! │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 30
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Delete me (14:33 ✓✓) │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 61
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) React to this │
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 97
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) React to this │
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 163
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
📌 02.01.2022 14:33 Important pinned message! Ctrl+P
|
||||
📌 20.12.2021 14:33 Important pinned message! Ctrl+P
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Regular message │
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 193
|
||||
expression: output
|
||||
---
|
||||
┌ Поиск по сообщениям ─────────────────────────────────────────────────────────┐
|
||||
│🔍 world█ (1/2) │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│▶ User (02.01.2022 14:33) │
|
||||
│▶ User (20.12.2021 14:33) │
|
||||
│ Hello world │
|
||||
│ │
|
||||
│ User (02.01.2022 14:33) │
|
||||
│ User (20.12.2021 14:33) │
|
||||
│ World is beautiful │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
Reference in New Issue
Block a user