Split core and TUI crates

This commit is contained in:
Mikhail Kilin
2026-05-20 00:31:18 +03:00
parent 91a8700b8e
commit eefac431e5
238 changed files with 624 additions and 191 deletions

17
Cargo.lock generated
View File

@@ -3942,6 +3942,22 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20b1c6703d2284b9d4ddb620cd350f726a1c43bb6f7801f4361b55db2421caa8"
[[package]]
name = "tele-core"
version = "0.1.0"
dependencies = [
"async-trait",
"base64",
"chrono",
"serde",
"serde_json",
"tdlib-rs",
"thiserror 1.0.69",
"tokio",
"tokio-test",
"tracing",
]
[[package]]
name = "tele-tui"
version = "0.1.0"
@@ -3964,6 +3980,7 @@ dependencies = [
"serde",
"serde_json",
"tdlib-rs",
"tele-core",
"termwright",
"thiserror 1.0.69",
"tokio",

View File

@@ -1,65 +1,4 @@
[package]
name = "tele-tui"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Terminal UI for Telegram with Vim-style navigation"
license = "MIT"
repository = "https://github.com/your-username/tele-tui"
keywords = ["telegram", "tui", "terminal", "cli"]
categories = ["command-line-utilities"]
default-run = "tele-tui"
[features]
default = ["clipboard", "url-open", "notifications", "images"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]
notifications = ["dep:notify-rust"]
images = ["dep:ratatui-image", "dep:image"]
test-support = []
[dependencies]
ratatui = "0.29"
crossterm = "0.28"
tdlib-rs = { version = "1.2.0", features = ["download-tdlib"] }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"
chrono = "0.4"
open = { version = "5.0", optional = true }
arboard = { version = "3.4", optional = true }
notify-rust = { version = "4.11", optional = true }
ratatui-image = { version = "8.1", optional = true, features = ["image-defaults"] }
image = { version = "0.25", optional = true }
toml = "0.8"
dirs = "5.0"
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
base64 = "0.22.1"
fs2 = "0.4"
[dev-dependencies]
insta = "1.34"
tokio-test = "0.4"
criterion = "0.5"
termwright = "0.2"
[[bin]]
name = "tele-tui-test-fixture"
path = "src/bin/tele-tui-test-fixture.rs"
required-features = ["test-support"]
[[bench]]
name = "group_messages"
harness = false
[[bench]]
name = "formatting"
harness = false
[[bench]]
name = "format_markdown"
harness = false
[workspace]
members = ["crates/tele-core", "crates/tele-tui"]
default-members = ["crates/tele-tui"]
resolver = "2"

View File

@@ -27,9 +27,11 @@ cargo check
```bash
cargo fmt -- --check
cargo check --all-targets --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo check -p tele-core
cargo test -p tele-core
cargo check -p tele-tui --all-targets --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
git diff --check
```

View File

@@ -22,7 +22,7 @@
- Для голосовых сообщений нужен `ffplay` из ffmpeg.
```bash
cargo build --release
cargo build -p tele-tui --release
```
## Credentials
@@ -54,6 +54,10 @@ cargo run --release
cargo run --release -- --account work
```
`Cargo.toml` в корне - workspace manifest. По умолчанию `cargo run` и `cargo test`
работают с `crates/tele-tui`; переиспользуемая TDLib-логика лежит в
`crates/tele-core`.
Runtime-конфиг создаётся в `~/.config/tele-tui/config.toml`; пример лежит в [config.toml.example](config.toml.example).
## Документация
@@ -64,6 +68,7 @@ Runtime-конфиг создаётся в `~/.config/tele-tui/config.toml`; п
- [docs/HOTKEYS.md](docs/HOTKEYS.md) - горячие клавиши.
- [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) - карта подсистем.
- [docs/TDLIB_INTEGRATION.md](docs/TDLIB_INTEGRATION.md) - проектные заметки по TDLib.
- [docs/IOS_CORE_REUSE.md](docs/IOS_CORE_REUSE.md) - граница `tele-core` для будущего iOS-клиента.
## Лицензия

View File

@@ -0,0 +1,29 @@
[package]
name = "tele-core"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Reusable Telegram/TDLib core for tele-tui"
license = "MIT"
repository = "https://github.com/your-username/tele-tui"
keywords = ["telegram", "tdlib"]
categories = ["api-bindings"]
[features]
default = []
images = []
test-support = []
[dependencies]
tdlib-rs = { version = "1.2.0", features = ["download-tdlib"] }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
thiserror = "1.0"
tracing = "0.1"
base64 = "0.22.1"
[dev-dependencies]
tokio-test = "0.4"

View File

@@ -0,0 +1,5 @@
//! Account profile data structures and validation.
pub mod profile;
pub use profile::{validate_account_name, AccountProfile, AccountsConfig};

View File

@@ -0,0 +1,114 @@
//! Account profile data structures and validation.
//!
//! Defines `AccountProfile` and `AccountsConfig` for multi-account support.
//! Account names are validated to contain only alphanumeric characters, hyphens, and underscores.
use serde::{Deserialize, Serialize};
/// Configuration for all accounts, stored in `~/.config/tele-tui/accounts.toml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountsConfig {
/// Name of the default account to use when no `--account` flag is provided.
pub default_account: String,
/// List of configured accounts.
pub accounts: Vec<AccountProfile>,
}
/// A single account profile.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountProfile {
/// Unique identifier (used in directory names and CLI flag).
pub name: String,
/// Human-readable display name.
pub display_name: String,
}
impl AccountsConfig {
/// Creates a default config with a single "default" account.
pub fn default_single() -> Self {
Self {
default_account: "default".to_string(),
accounts: vec![AccountProfile {
name: "default".to_string(),
display_name: "Default".to_string(),
}],
}
}
/// Finds an account by name.
pub fn find_account(&self, name: &str) -> Option<&AccountProfile> {
self.accounts.iter().find(|a| a.name == name)
}
}
/// Validates an account name.
///
/// Valid names contain only lowercase alphanumeric characters, hyphens, and underscores.
/// Must be 1-32 characters long.
///
/// # Errors
///
/// Returns a descriptive error message if the name is invalid.
pub fn validate_account_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("Account name cannot be empty".to_string());
}
if name.len() > 32 {
return Err("Account name cannot be longer than 32 characters".to_string());
}
if !name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
{
return Err(
"Account name can only contain lowercase letters, digits, hyphens, and underscores"
.to_string(),
);
}
if name.starts_with('-') || name.starts_with('_') {
return Err("Account name cannot start with a hyphen or underscore".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_account_name_valid() {
assert!(validate_account_name("default").is_ok());
assert!(validate_account_name("work").is_ok());
assert!(validate_account_name("my-account").is_ok());
assert!(validate_account_name("account_2").is_ok());
assert!(validate_account_name("a").is_ok());
}
#[test]
fn test_validate_account_name_invalid() {
assert!(validate_account_name("").is_err());
assert!(validate_account_name("My Account").is_err());
assert!(validate_account_name("UPPER").is_err());
assert!(validate_account_name("with spaces").is_err());
assert!(validate_account_name("-starts-with-dash").is_err());
assert!(validate_account_name("_starts-with-underscore").is_err());
assert!(validate_account_name(&"a".repeat(33)).is_err());
}
#[test]
fn test_default_single_config() {
let config = AccountsConfig::default_single();
assert_eq!(config.default_account, "default");
assert_eq!(config.accounts.len(), 1);
assert_eq!(config.accounts[0].name, "default");
}
#[test]
fn test_find_account() {
let config = AccountsConfig::default_single();
assert!(config.find_account("default").is_some());
assert!(config.find_account("nonexistent").is_none());
}
}

View File

@@ -0,0 +1,6 @@
pub const MAX_MESSAGES_IN_CHAT: usize = 500;
pub const MAX_USER_CACHE_SIZE: usize = 500;
pub const MAX_CHATS: usize = 200;
pub const MAX_CHAT_USER_IDS: usize = 500;
pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;

View File

@@ -0,0 +1,11 @@
//! Reusable Telegram/TDLib core for tele-tui and future clients.
mod constants;
mod utils;
pub mod accounts;
pub mod message_grouping;
pub mod tdlib;
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
pub mod types;

View File

@@ -35,10 +35,10 @@ pub enum MessageGroup<'a> {
/// # Примеры
///
/// ```no_run
/// use tele_tui::message_grouping::{group_messages, MessageGroup};
/// use tele_core::message_grouping::{group_messages, MessageGroup};
///
/// # use tele_tui::tdlib::types::MessageBuilder;
/// # use tele_tui::types::MessageId;
/// # use tele_core::tdlib::types::MessageBuilder;
/// # use tele_core::types::MessageId;
/// # let msg = MessageBuilder::new(MessageId::new(1)).sender_name("Alice").text("Hello").build();
/// let messages = vec![msg];
/// let grouped = group_messages(&messages);

View File

@@ -1,5 +1,5 @@
use crate::types::{ChatId, MessageId, UserId};
use std::env;
use std::collections::VecDeque;
use std::path::PathBuf;
use tdlib_rs::enums::{Chat as TdChat, ChatList, ConnectionState, Update, UserStatus};
use tdlib_rs::functions;
@@ -13,7 +13,25 @@ use super::types::{
ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus,
};
use super::users::UserCache;
use crate::notifications::NotificationManager;
#[derive(Debug, Clone)]
pub struct TdCredentials {
pub api_id: i32,
pub api_hash: String,
}
#[derive(Debug, Clone)]
pub struct TdClientConfig {
pub credentials: TdCredentials,
pub db_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct IncomingMessageEvent {
pub chat: ChatInfo,
pub message: MessageInfo,
pub sender_name: String,
}
/// TDLib client wrapper for Telegram integration.
///
@@ -28,9 +46,15 @@ use crate::notifications::NotificationManager;
/// # Examples
///
/// ```ignore
/// use tele_tui::tdlib::TdClient;
/// use tele_core::tdlib::TdClient;
///
/// let mut client = TdClient::new(std::path::PathBuf::from("tdlib_data"));
/// let mut client = TdClient::new(tele_core::tdlib::TdClientConfig {
/// credentials: tele_core::tdlib::TdCredentials {
/// api_id: 123,
/// api_hash: "hash".to_string(),
/// },
/// db_path: std::path::PathBuf::from("tdlib_data"),
/// });
///
/// // Start authorization
/// client.send_phone_number("+1234567890".to_string()).await?;
@@ -52,7 +76,7 @@ pub struct TdClient {
pub message_manager: MessageManager,
pub user_cache: UserCache,
pub reaction_manager: ReactionManager,
pub notification_manager: NotificationManager,
incoming_message_events: VecDeque<IncomingMessageEvent>,
// Состояние сети
pub network_state: NetworkState,
@@ -62,62 +86,41 @@ pub struct TdClient {
impl TdClient {
/// Creates a new TDLib client instance.
///
/// 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.
///
/// # Returns
///
/// A new `TdClient` instance ready for authentication.
pub fn new(db_path: PathBuf) -> 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")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
let api_hash = env::var("API_HASH").unwrap_or_default();
(api_id, api_hash)
});
pub fn new(config: TdClientConfig) -> Self {
let client_id = tdlib_rs::create_client();
Self {
api_id,
api_hash,
db_path,
api_id: config.credentials.api_id,
api_hash: config.credentials.api_hash,
db_path: config.db_path,
client_id,
auth: AuthManager::new(client_id),
chat_manager: ChatManager::new(client_id),
message_manager: MessageManager::new(client_id),
user_cache: UserCache::new(client_id),
reaction_manager: ReactionManager::new(client_id),
notification_manager: NotificationManager::new(),
incoming_message_events: VecDeque::new(),
network_state: NetworkState::Connecting,
}
}
/// Configures notification manager from app config
pub fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) {
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());
pub fn enqueue_incoming_message_event(
&mut self,
chat: ChatInfo,
message: MessageInfo,
sender_name: String,
) {
self.incoming_message_events
.push_back(IncomingMessageEvent { chat, message, sender_name });
}
/// Synchronizes muted chats from Telegram to notification manager.
///
/// Should be called after chats are loaded to ensure muted chats don't trigger notifications.
pub fn sync_notification_muted_chats(&mut self) {
self.notification_manager
.sync_muted_chats(&self.chat_manager.chats);
pub fn drain_incoming_message_events(&mut self) -> Vec<IncomingMessageEvent> {
self.incoming_message_events.drain(..).collect()
}
// Делегирование к auth
@@ -791,7 +794,13 @@ impl TdClient {
let _ = functions::close(self.client_id).await;
// 2. Create new client
let new_client = TdClient::new(db_path);
let new_client = TdClient::new(TdClientConfig {
credentials: TdCredentials {
api_id: self.api_id,
api_hash: self.api_hash.clone(),
},
db_path,
});
// 3. Spawn set_tdlib_parameters for new client
let new_client_id = new_client.client_id;

View File

@@ -5,7 +5,7 @@
use super::client::TdClient;
use super::r#trait::{
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
MessageClient, NotificationClient, ReactionClient, UpdateClient, UserClient,
MessageClient, ReactionClient, UpdateClient, UserClient,
};
use super::{
AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache,
@@ -313,17 +313,6 @@ impl ClientState for TdClient {
}
}
impl NotificationClient for TdClient {
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);
}
}
#[async_trait]
impl AccountClient for TdClient {
async fn recreate_client(&mut self, db_path: PathBuf) -> Result<(), String> {
@@ -336,4 +325,8 @@ impl UpdateClient for TdClient {
// Delegate to the real implementation
TdClient::handle_update(self, update)
}
fn drain_incoming_message_events(&mut self) -> Vec<super::IncomingMessageEvent> {
TdClient::drain_incoming_message_events(self)
}
}

View File

@@ -4,7 +4,7 @@ mod chat_helpers; // Chat management helpers
pub mod chats;
pub mod client;
mod client_impl; // Private module for trait implementation
pub(crate) mod message_conversion; // Message conversion utilities (for messages.rs)
pub mod message_conversion; // Message conversion utilities (for messages.rs)
mod message_converter; // Message conversion utilities (for client.rs)
pub mod messages;
pub mod reactions;
@@ -19,7 +19,7 @@ pub use client::TdClient;
#[allow(unused_imports)]
pub use r#trait::{
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
MessageClient, NotificationClient, ReactionClient, TdClientTrait, UpdateClient, UserClient,
MessageClient, ReactionClient, TdClientTrait, UpdateClient, UserClient,
};
#[allow(unused_imports)]
pub use types::{
@@ -28,6 +28,7 @@ pub use types::{
VoiceDownloadState, VoiceInfo,
};
pub use client::{IncomingMessageEvent, TdClientConfig, TdCredentials};
#[cfg(feature = "images")]
pub use types::ImageModalState;
pub use users::UserCache;

View File

@@ -3,7 +3,10 @@
//! This trait allows tests to use FakeTdClient instead of real TDLib client.
#![allow(dead_code)]
use crate::tdlib::{AuthState, FolderInfo, MessageInfo, ProfileInfo, UserCache, UserOnlineStatus};
use crate::tdlib::{
AuthState, FolderInfo, IncomingMessageEvent, MessageInfo, ProfileInfo, UserCache,
UserOnlineStatus,
};
use crate::types::{ChatId, MessageId, UserId};
use async_trait::async_trait;
use std::borrow::Cow;
@@ -163,12 +166,6 @@ pub trait ClientState: Send {
fn network_state(&self) -> super::types::NetworkState;
}
/// Notification configuration operations.
pub trait NotificationClient: Send {
fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig);
fn sync_notification_muted_chats(&mut self);
}
/// Account switching operations.
#[async_trait]
pub trait AccountClient: Send {
@@ -182,6 +179,7 @@ pub trait AccountClient: Send {
/// TDLib update routing.
pub trait UpdateClient: Send {
fn handle_update(&mut self, update: Update);
fn drain_incoming_message_events(&mut self) -> Vec<IncomingMessageEvent>;
}
/// Facade trait for TDLib client operations
@@ -198,7 +196,6 @@ pub trait TdClientTrait:
+ ReactionClient
+ FileClient
+ ClientState
+ NotificationClient
+ AccountClient
+ UpdateClient
+ Send
@@ -214,7 +211,6 @@ impl<T> TdClientTrait for T where
+ ReactionClient
+ FileClient
+ ClientState
+ NotificationClient
+ AccountClient
+ UpdateClient
+ Send

View File

@@ -312,8 +312,8 @@ impl MessageInfo {
/// # Примеры
///
/// ```
/// use tele_tui::tdlib::MessageBuilder;
/// use tele_tui::types::MessageId;
/// use tele_core::tdlib::MessageBuilder;
/// use tele_core::types::MessageId;
///
/// let message = MessageBuilder::new(MessageId::new(123))
/// .sender_name("Alice")

View File

@@ -27,12 +27,9 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag
crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id);
// Get sender name (from message or user cache)
let sender_name = msg_info.sender_name();
let sender_name = msg_info.sender_name().to_string();
// Send notification
let _ = client
.notification_manager
.notify_new_message(&chat, &msg_info, sender_name);
client.enqueue_incoming_message_event(chat, msg_info, sender_name);
}
return;
}

View File

@@ -3,7 +3,7 @@
use super::fake_tdclient::FakeTdClient;
use crate::tdlib::{
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
MessageClient, NotificationClient, ReactionClient, UpdateClient, UserClient,
MessageClient, ReactionClient, UpdateClient, UserClient,
};
use crate::tdlib::{
AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache,
@@ -342,12 +342,6 @@ impl ClientState for FakeTdClient {
}
}
impl NotificationClient for FakeTdClient {
fn configure_notifications(&mut self, _config: &crate::config::NotificationsConfig) {}
fn sync_notification_muted_chats(&mut self) {}
}
#[async_trait]
impl AccountClient for FakeTdClient {
async fn recreate_client(&mut self, _db_path: PathBuf) -> Result<(), String> {
@@ -357,4 +351,8 @@ impl AccountClient for FakeTdClient {
impl UpdateClient for FakeTdClient {
fn handle_update(&mut self, _update: Update) {}
fn drain_incoming_message_events(&mut self) -> Vec<crate::tdlib::IncomingMessageEvent> {
Vec::new()
}
}

View File

@@ -0,0 +1,7 @@
//! Core test support for deterministic TDLib fixtures.
pub mod fake_tdclient;
mod fake_tdclient_impl;
pub mod test_data;
pub use fake_tdclient::FakeTdClient;

View File

@@ -0,0 +1,9 @@
use chrono::{DateTime, Local, NaiveDate, Utc};
pub fn get_day(timestamp: i32) -> i64 {
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch date");
let msg_day = DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
.map(|dt| dt.with_timezone(&Local).date_naive())
.unwrap_or(epoch);
msg_day.signed_duration_since(epoch).num_days()
}

View File

@@ -0,0 +1,66 @@
[package]
name = "tele-tui"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Terminal UI for Telegram with Vim-style navigation"
license = "MIT"
repository = "https://github.com/your-username/tele-tui"
keywords = ["telegram", "tui", "terminal", "cli"]
categories = ["command-line-utilities"]
default-run = "tele-tui"
[features]
default = ["clipboard", "url-open", "notifications", "images"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]
notifications = ["dep:notify-rust"]
images = ["dep:ratatui-image", "dep:image", "tele-core/images"]
test-support = ["tele-core/test-support"]
[dependencies]
tele-core = { path = "../tele-core" }
ratatui = "0.29"
crossterm = "0.28"
tdlib-rs = { version = "1.2.0", features = ["download-tdlib"] }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"
chrono = "0.4"
open = { version = "5.0", optional = true }
arboard = { version = "3.4", optional = true }
notify-rust = { version = "4.11", optional = true }
ratatui-image = { version = "8.1", optional = true, features = ["image-defaults"] }
image = { version = "0.25", optional = true }
toml = "0.8"
dirs = "5.0"
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
base64 = "0.22.1"
fs2 = "0.4"
[dev-dependencies]
insta = "1.34"
tokio-test = "0.4"
criterion = "0.5"
termwright = "0.2"
[[bin]]
name = "tele-tui-test-fixture"
path = "src/bin/tele-tui-test-fixture.rs"
required-features = ["test-support"]
[[bench]]
name = "group_messages"
harness = false
[[bench]]
name = "formatting"
harness = false
[[bench]]
name = "format_markdown"
harness = false

View File

@@ -13,7 +13,12 @@ fn main() {
fn tdlib_lib_dirs() -> Vec<PathBuf> {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
let build_dir = manifest_dir.join("target").join(profile).join("build");
let workspace_dir = manifest_dir
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf)
.unwrap_or(manifest_dir);
let build_dir = workspace_dir.join("target").join(profile).join("build");
let Ok(entries) = fs::read_dir(build_dir) else {
return Vec::new();

View File

@@ -15,7 +15,9 @@ pub use methods::*;
pub use state::AppScreen;
use crate::accounts::AccountProfile;
use crate::notifications::NotificationManager;
use crate::tdlib::{ChatInfo, TdClient, TdClientTrait};
use crate::tdlib::{TdClientConfig, TdCredentials};
use crate::types::{ChatId, MessageId};
use ratatui::widgets::ListState;
use std::path::PathBuf;
@@ -102,6 +104,7 @@ pub struct App<T: TdClientTrait = TdClient> {
config: crate::config::Config,
pub screen: AppScreen,
pub td_client: T,
pub notification_manager: NotificationManager,
/// Состояние чата - type-safe state machine (новое!)
pub chat_state: ChatState,
/// Vim-like input mode: Normal (navigation) / Insert (text input)
@@ -205,11 +208,13 @@ impl<T: TdClientTrait> App<T> {
let inline_image_renderer = crate::media::image_renderer::ImageRenderer::new_fast();
#[cfg(feature = "images")]
let modal_image_renderer = crate::media::image_renderer::ImageRenderer::new();
let notification_manager = NotificationManager::from_config(&config.notifications);
App {
config,
screen: AppScreen::Loading,
td_client,
notification_manager,
chat_state: ChatState::Normal,
input_mode: InputMode::Normal,
phone_input: String::new(),
@@ -613,8 +618,18 @@ 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> {
let mut td_client = TdClient::new(db_path);
td_client.configure_notifications(&config.notifications);
let (api_id, api_hash) = crate::config::Config::load_credentials().unwrap_or_else(|_| {
let api_id = std::env::var("API_ID")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
let api_hash = std::env::var("API_HASH").unwrap_or_default();
(api_id, api_hash)
});
let td_client = TdClient::new(TdClientConfig {
credentials: TdCredentials { api_id, api_hash },
db_path,
});
App::with_client(config, td_client)
}
}

View File

@@ -5,15 +5,19 @@
// ============================================================================
/// Максимальное количество сообщений в одном чате (для оптимизации памяти)
#[allow(dead_code)]
pub const MAX_MESSAGES_IN_CHAT: usize = 500;
/// Максимальный размер кэша пользователей (LRU)
#[allow(dead_code)]
pub const MAX_USER_CACHE_SIZE: usize = 500;
/// Максимальное количество чатов для загрузки
#[allow(dead_code)]
pub const MAX_CHATS: usize = 200;
/// Максимальное количество user_ids для хранения в чате
#[allow(dead_code)]
pub const MAX_CHAT_USER_IDS: usize = 500;
// ============================================================================
@@ -27,6 +31,7 @@ pub const POLL_TIMEOUT_MS: u64 = 16;
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2;
/// Количество пользователей для ленивой загрузки за один тик
#[allow(dead_code)]
pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
// ============================================================================
@@ -34,6 +39,7 @@ pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
// ============================================================================
/// Лимит количества сообщений для загрузки через TDLib за раз
#[allow(dead_code)]
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
// ============================================================================

View File

@@ -50,7 +50,8 @@ pub async fn handle_global_commands<T: TdClientTrait>(app: &mut App<T>, key: Key
let _ =
with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await;
// Синхронизируем muted чаты после обновления
app.td_client.sync_notification_muted_chats();
app.notification_manager
.sync_muted_chats(app.td_client.chats());
app.status_message = None;
true
}

View File

@@ -11,11 +11,10 @@ pub mod formatting;
pub mod input;
#[cfg(feature = "images")]
pub mod media;
pub mod message_grouping;
pub mod notifications;
pub mod tdlib;
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
pub mod types;
pub mod ui;
pub mod utils;
pub use tele_core::{message_grouping, tdlib, types};

View File

@@ -7,13 +7,12 @@ mod formatting;
mod input;
#[cfg(feature = "images")]
mod media;
mod message_grouping;
mod notifications;
mod tdlib;
mod types;
mod ui;
mod utils;
pub use tele_core::{message_grouping, tdlib, types};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
@@ -202,6 +201,13 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
);
}
}
for event in app.td_client.drain_incoming_message_events() {
let _ = app.notification_manager.notify_new_message(
&event.chat,
&event.message,
&event.sender_name,
);
}
// Помечаем UI как требующий перерисовки если были обновления
if had_updates {
@@ -437,7 +443,7 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
continue;
}
let notifications_cfg = app.config().notifications.clone();
app.td_client.configure_notifications(&notifications_cfg);
app.notification_manager.configure(&notifications_cfg);
// 4. Reset app state
app.current_account_name = account_name.clone();
@@ -509,7 +515,8 @@ async fn update_screen_state<T: tdlib::TdClientTrait>(app: &mut App<T>) -> bool
app.chat_list_state.select(Some(0));
}
// Синхронизируем muted чаты для notifications
app.td_client.sync_notification_muted_chats();
app.notification_manager
.sync_muted_chats(app.td_client.chats());
// Убираем статус загрузки когда чаты появились
if app.is_loading {
app.is_loading = false;

View File

@@ -52,6 +52,22 @@ impl NotificationManager {
}
}
/// Creates a notification manager from application config.
pub fn from_config(config: &crate::config::NotificationsConfig) -> Self {
let mut manager = Self::new();
manager.configure(config);
manager
}
/// Applies notification settings from application config.
pub fn configure(&mut self, config: &crate::config::NotificationsConfig) {
self.set_enabled(config.enabled);
self.set_only_mentions(config.only_mentions);
self.set_show_preview(config.show_preview);
self.set_timeout(config.timeout_ms);
self.set_urgency(config.urgency.clone());
}
/// Sets whether notifications are enabled
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;

View File

@@ -1,9 +1,6 @@
//! Test-only support for deterministic UI fixtures and integration tests.
pub mod app_builder;
pub mod fake_tdclient;
mod fake_tdclient_impl;
pub mod snapshot_utils;
pub mod test_data;
pub use fake_tdclient::FakeTdClient;
pub use tele_core::test_support::{fake_tdclient, test_data, FakeTdClient};

Some files were not shown because too many files have changed in this diff Show More