yet-another-changes #10
28
CONTEXT.md
28
CONTEXT.md
@@ -38,6 +38,19 @@
|
|||||||
- **Индикатор редактирования**: ✎ рядом с временем для отредактированных сообщений
|
- **Индикатор редактирования**: ✎ рядом с временем для отредактированных сообщений
|
||||||
- **Новые сообщения в реальном времени** при открытом чате
|
- **Новые сообщения в реальном времени** при открытом чате
|
||||||
- **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username
|
- **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username
|
||||||
|
- **Typing indicator** ("печатает..."): отображение статуса набора текста собеседником и отправка своего статуса
|
||||||
|
- **Закреплённые сообщения**: отображение pinned message вверху чата с переходом к нему
|
||||||
|
- **Поиск по сообщениям в чате** (Ctrl+F): поиск текста внутри открытого чата с навигацией по результатам
|
||||||
|
- **Черновики**: автосохранение набранного текста при переключении между чатами
|
||||||
|
- **Профиль пользователя/чата** (`i`): просмотр информации о собеседнике или группе
|
||||||
|
- **Копирование сообщений** (`y`/`н`): копирование текста сообщения в системный буфер обмена
|
||||||
|
- **Реакции на сообщения**:
|
||||||
|
- Отображение реакций под сообщениями
|
||||||
|
- Логика отображения: 1 человек = только emoji, 2+ = emoji + счётчик
|
||||||
|
- Свои реакции в рамках [👍], чужие без рамок 👍
|
||||||
|
- Emoji picker с сеткой доступных реакций (8 в ряду)
|
||||||
|
- Добавление/удаление реакций (toggle)
|
||||||
|
- Обновление реакций в реальном времени через Update::MessageInteractionInfo
|
||||||
- **Кеширование имён пользователей**: имена загружаются асинхронно и обновляются в UI
|
- **Кеширование имён пользователей**: имена загружаются асинхронно и обновляются в UI
|
||||||
- **Папки Telegram**: загрузка и переключение между папками (1-9)
|
- **Папки Telegram**: загрузка и переключение между папками (1-9)
|
||||||
- **Медиа-заглушки**: [Фото], [Видео], [Голосовое], [Стикер], [GIF] и др.
|
- **Медиа-заглушки**: [Фото], [Видео], [Голосовое], [Стикер], [GIF] и др.
|
||||||
@@ -88,6 +101,14 @@
|
|||||||
- `n` / `т` / `Esc` — отменить удаление в модалке
|
- `n` / `т` / `Esc` — отменить удаление в модалке
|
||||||
- `Esc` — отменить выбор/редактирование/reply
|
- `Esc` — отменить выбор/редактирование/reply
|
||||||
- `1-9` — переключение папок (в списке чатов)
|
- `1-9` — переключение папок (в списке чатов)
|
||||||
|
- `Ctrl+F` — поиск по сообщениям в открытом чате
|
||||||
|
- `n` / `N` — навигация по результатам поиска (следующий/предыдущий)
|
||||||
|
- `i` — открыть профиль пользователя/чата
|
||||||
|
- `y` / `н` в режиме выбора — скопировать сообщение в буфер обмена
|
||||||
|
- `e` / `у` в режиме выбора — добавить реакцию (открывает emoji picker)
|
||||||
|
- `←` / `→` / `↑` / `↓` в emoji picker — навигация по сетке реакций
|
||||||
|
- `Enter` в emoji picker — добавить/удалить реакцию
|
||||||
|
- `Esc` в emoji picker — закрыть picker
|
||||||
- **Редактирование текста в инпуте:**
|
- **Редактирование текста в инпуте:**
|
||||||
- `←` / `→` — перемещение курсора
|
- `←` / `→` — перемещение курсора
|
||||||
- `Home` — курсор в начало
|
- `Home` — курсор в начало
|
||||||
@@ -163,13 +184,6 @@ API_HASH=your_api_hash
|
|||||||
|
|
||||||
## Что НЕ сделано / TODO (Фаза 9)
|
## Что НЕ сделано / TODO (Фаза 9)
|
||||||
|
|
||||||
- [ ] Typing indicator ("печатает...")
|
|
||||||
- [ ] Закреплённые сообщения (Pinned) — отображение вверху чата
|
|
||||||
- [ ] Поиск по сообщениям в чате (Ctrl+F)
|
|
||||||
- [ ] Черновики — сохранение текста при переключении чатов
|
|
||||||
- [ ] Профиль пользователя/чата (хоткей `i`)
|
|
||||||
- [ ] Копирование сообщений в буфер обмена (`y` в режиме выбора)
|
|
||||||
- [ ] Реакции — просмотр и добавление
|
|
||||||
- [ ] Конфигурационный файл (~/.config/tele-tui/config.toml)
|
- [ ] Конфигурационный файл (~/.config/tele-tui/config.toml)
|
||||||
|
|
||||||
## Известные проблемы
|
## Известные проблемы
|
||||||
|
|||||||
359
Cargo.lock
generated
359
Cargo.lock
generated
@@ -43,6 +43,26 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arboard"
|
||||||
|
version = "3.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||||
|
dependencies = [
|
||||||
|
"clipboard-win",
|
||||||
|
"image",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-foundation",
|
||||||
|
"parking_lot",
|
||||||
|
"percent-encoding",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"x11rb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -82,12 +102,24 @@ version = "3.19.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -130,9 +162,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.53"
|
version = "1.2.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
|
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -170,6 +202,15 @@ dependencies = [
|
|||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clipboard-win"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||||
|
dependencies = [
|
||||||
|
"error-code",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "compact_str"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -270,6 +311,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -408,6 +455,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dispatch2"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -462,12 +519,47 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "error-code"
|
||||||
|
version = "3.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fax"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
|
||||||
|
dependencies = [
|
||||||
|
"fax_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fax_derive"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -580,6 +672,16 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gethostname"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||||
|
dependencies = [
|
||||||
|
"rustix 1.1.3",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -624,6 +726,17 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -919,6 +1032,20 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"moxcms",
|
||||||
|
"num-traits",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -1156,6 +1283,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moxcms"
|
||||||
|
version = "0.7.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -1175,9 +1312,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
@@ -1188,6 +1325,79 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-app-kit"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"dispatch2",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-graphics"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"dispatch2",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-io-surface"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -1324,6 +1534,19 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -1341,18 +1564,33 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "pxfm"
|
||||||
version = "1.0.43"
|
version = "0.1.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -1767,9 +2005,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -1911,6 +2149,7 @@ checksum = "87cbdfae498e57fb48d380fff8eb5c9c98d4497c998f6de0d30d5d6b12f5358b"
|
|||||||
name = "tele-tui"
|
name = "tele-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arboard",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@@ -1937,18 +2176,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1956,10 +2195,24 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "tiff"
|
||||||
version = "0.3.45"
|
version = "0.10.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
|
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
|
||||||
|
dependencies = [
|
||||||
|
"fax",
|
||||||
|
"flate2",
|
||||||
|
"half",
|
||||||
|
"quick-error",
|
||||||
|
"weezl",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -1972,15 +2225,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.25"
|
version = "0.2.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
|
checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
@@ -2297,6 +2550,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "weezl"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -2566,6 +2825,23 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x11rb"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||||
|
dependencies = [
|
||||||
|
"gethostname",
|
||||||
|
"rustix 1.1.3",
|
||||||
|
"x11rb-protocol",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x11rb-protocol"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xz2"
|
name = "xz2"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -2598,6 +2874,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -2704,9 +3000,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.15"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
|
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
@@ -2747,3 +3043,18 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-core"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-jpeg"
|
||||||
|
version = "0.4.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||||||
|
dependencies = [
|
||||||
|
"zune-core",
|
||||||
|
]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ serde_json = "1.0"
|
|||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
open = "5.0"
|
open = "5.0"
|
||||||
|
arboard = "3.4"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tdlib-rs = { version = "1.1", features = ["download-tdlib"] }
|
tdlib-rs = { version = "1.1", features = ["download-tdlib"] }
|
||||||
|
|||||||
16
ROADMAP.md
16
ROADMAP.md
@@ -112,30 +112,30 @@
|
|||||||
- Esc для отмены
|
- Esc для отмены
|
||||||
- Отображение "↪ Переслано от" для пересланных сообщений
|
- Отображение "↪ Переслано от" для пересланных сообщений
|
||||||
|
|
||||||
## Фаза 9: Расширенные возможности [TODO]
|
## Фаза 9: Расширенные возможности [IN PROGRESS]
|
||||||
|
|
||||||
- [ ] Typing indicator ("печатает...")
|
- [x] Typing indicator ("печатает...")
|
||||||
- Показывать когда собеседник печатает
|
- Показывать когда собеседник печатает
|
||||||
- Отправлять свой статус печати при наборе текста
|
- Отправлять свой статус печати при наборе текста
|
||||||
- [ ] Закреплённые сообщения (Pinned)
|
- [x] Закреплённые сообщения (Pinned)
|
||||||
- Отображать pinned message вверху открытого чата
|
- Отображать pinned message вверху открытого чата
|
||||||
- Клик/хоткей для перехода к закреплённому сообщению
|
- Клик/хоткей для перехода к закреплённому сообщению
|
||||||
- [ ] Поиск по сообщениям в чате
|
- [x] Поиск по сообщениям в чате
|
||||||
- `Ctrl+F` — поиск текста внутри открытого чата
|
- `Ctrl+F` — поиск текста внутри открытого чата
|
||||||
- Навигация по результатам (n/N или стрелки)
|
- Навигация по результатам (n/N или стрелки)
|
||||||
- Подсветка найденных совпадений
|
- Подсветка найденных совпадений
|
||||||
- [ ] Черновики
|
- [x] Черновики
|
||||||
- Сохранять набранный текст при переключении между чатами
|
- Сохранять набранный текст при переключении между чатами
|
||||||
- Индикатор черновика в списке чатов
|
- Индикатор черновика в списке чатов
|
||||||
- Восстановление текста при возврате в чат
|
- Восстановление текста при возврате в чат
|
||||||
- [ ] Профиль пользователя/чата
|
- [x] Профиль пользователя/чата
|
||||||
- `i` — открыть информацию о чате/собеседнике
|
- `i` — открыть информацию о чате/собеседнике
|
||||||
- Для личных чатов: имя, username, телефон, био
|
- Для личных чатов: имя, username, телефон, био
|
||||||
- Для групп: название, описание, количество участников
|
- Для групп: название, описание, количество участников
|
||||||
- [ ] Копирование сообщений
|
- [x] Копирование сообщений
|
||||||
- `y` / `н` в режиме выбора — скопировать текст в системный буфер обмена
|
- `y` / `н` в режиме выбора — скопировать текст в системный буфер обмена
|
||||||
- Использовать clipboard crate для кроссплатформенности
|
- Использовать clipboard crate для кроссплатформенности
|
||||||
- [ ] Реакции
|
- [x] Реакции
|
||||||
- Отображение реакций под сообщениями
|
- Отображение реакций под сообщениями
|
||||||
- `e` в режиме выбора — добавить реакцию (emoji picker)
|
- `e` в режиме выбора — добавить реакцию (emoji picker)
|
||||||
- Список доступных реакций чата
|
- Список доступных реакций чата
|
||||||
|
|||||||
@@ -75,8 +75,18 @@ pub struct App {
|
|||||||
pub leave_group_confirmation_step: u8,
|
pub leave_group_confirmation_step: u8,
|
||||||
/// Информация профиля для отображения
|
/// Информация профиля для отображения
|
||||||
pub profile_info: Option<crate::tdlib::ProfileInfo>,
|
pub profile_info: Option<crate::tdlib::ProfileInfo>,
|
||||||
|
// Reaction picker mode
|
||||||
|
/// Режим выбора реакции
|
||||||
|
pub is_reaction_picker_mode: bool,
|
||||||
|
/// ID сообщения для добавления реакции
|
||||||
|
pub selected_message_for_reaction: Option<i64>,
|
||||||
|
/// Список доступных реакций
|
||||||
|
pub available_reactions: Vec<String>,
|
||||||
|
/// Индекс выбранной реакции в picker
|
||||||
|
pub selected_reaction_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> App {
|
pub fn new() -> App {
|
||||||
let mut state = ListState::default();
|
let mut state = ListState::default();
|
||||||
@@ -119,6 +129,10 @@ impl App {
|
|||||||
selected_profile_action: 0,
|
selected_profile_action: 0,
|
||||||
leave_group_confirmation_step: 0,
|
leave_group_confirmation_step: 0,
|
||||||
profile_info: None,
|
profile_info: None,
|
||||||
|
is_reaction_picker_mode: false,
|
||||||
|
selected_message_for_reaction: None,
|
||||||
|
available_reactions: Vec::new(),
|
||||||
|
selected_reaction_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,4 +620,44 @@ impl App {
|
|||||||
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
||||||
self.leave_group_confirmation_step
|
self.leave_group_confirmation_step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Reaction Picker ==========
|
||||||
|
|
||||||
|
pub fn is_reaction_picker_mode(&self) -> bool {
|
||||||
|
self.is_reaction_picker_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter_reaction_picker_mode(&mut self, message_id: i64, available_reactions: Vec<String>) {
|
||||||
|
self.is_reaction_picker_mode = true;
|
||||||
|
self.selected_message_for_reaction = Some(message_id);
|
||||||
|
self.available_reactions = available_reactions;
|
||||||
|
self.selected_reaction_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_reaction_picker_mode(&mut self) {
|
||||||
|
self.is_reaction_picker_mode = false;
|
||||||
|
self.selected_message_for_reaction = None;
|
||||||
|
self.available_reactions.clear();
|
||||||
|
self.selected_reaction_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_reaction(&mut self) {
|
||||||
|
if !self.available_reactions.is_empty() && self.selected_reaction_index > 0 {
|
||||||
|
self.selected_reaction_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_reaction(&mut self) {
|
||||||
|
if self.selected_reaction_index + 1 < self.available_reactions.len() {
|
||||||
|
self.selected_reaction_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_reaction(&self) -> Option<&String> {
|
||||||
|
self.available_reactions.get(self.selected_reaction_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_message_for_reaction(&self) -> Option<i64> {
|
||||||
|
self.selected_message_for_reaction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,73 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обработка ввода в режиме выбора реакции
|
||||||
|
if app.is_reaction_picker_mode() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Left => {
|
||||||
|
app.select_previous_reaction();
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
app.select_next_reaction();
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
// Переход на ряд выше (8 эмодзи в ряду)
|
||||||
|
if app.selected_reaction_index >= 8 {
|
||||||
|
app.selected_reaction_index = app.selected_reaction_index.saturating_sub(8);
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
// Переход на ряд ниже (8 эмодзи в ряду)
|
||||||
|
let new_index = app.selected_reaction_index + 8;
|
||||||
|
if new_index < app.available_reactions.len() {
|
||||||
|
app.selected_reaction_index = new_index;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
// Добавить/убрать реакцию
|
||||||
|
if let Some(emoji) = app.get_selected_reaction().cloned() {
|
||||||
|
if let Some(message_id) = app.get_selected_message_for_reaction() {
|
||||||
|
if let Some(chat_id) = app.selected_chat_id {
|
||||||
|
app.status_message = Some("Отправка реакции...".to_string());
|
||||||
|
app.needs_redraw = true;
|
||||||
|
|
||||||
|
match timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
app.td_client.toggle_reaction(chat_id, message_id, emoji.clone())
|
||||||
|
).await {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
|
app.status_message = Some(format!("Реакция {} добавлена", emoji));
|
||||||
|
app.exit_reaction_picker_mode();
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
app.error_message = Some(format!("Ошибка: {}", e));
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
app.error_message = Some("Таймаут отправки реакции".to_string());
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.exit_reaction_picker_mode();
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Модалка подтверждения удаления
|
// Модалка подтверждения удаления
|
||||||
if app.is_confirm_delete_shown() {
|
if app.is_confirm_delete_shown() {
|
||||||
match key.code {
|
match key.code {
|
||||||
@@ -563,6 +630,58 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
// Начать режим пересылки
|
// Начать режим пересылки
|
||||||
app.start_forward_selected();
|
app.start_forward_selected();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('y') | KeyCode::Char('н') => {
|
||||||
|
// Копировать сообщение
|
||||||
|
if let Some(msg) = app.get_selected_message() {
|
||||||
|
let text = format_message_for_clipboard(msg);
|
||||||
|
match copy_to_clipboard(&text) {
|
||||||
|
Ok(_) => {
|
||||||
|
app.status_message = Some("Сообщение скопировано".to_string());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.error_message = Some(format!("Ошибка копирования: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('e') | KeyCode::Char('у') => {
|
||||||
|
// Открыть emoji picker для добавления реакции
|
||||||
|
if let Some(msg) = app.get_selected_message() {
|
||||||
|
let chat_id = app.selected_chat_id.unwrap();
|
||||||
|
let message_id = msg.id;
|
||||||
|
|
||||||
|
app.status_message = Some("Загрузка реакций...".to_string());
|
||||||
|
app.needs_redraw = true;
|
||||||
|
|
||||||
|
// Запрашиваем доступные реакции
|
||||||
|
match timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
app.td_client.get_message_available_reactions(chat_id, message_id)
|
||||||
|
).await {
|
||||||
|
Ok(Ok(reactions)) => {
|
||||||
|
if reactions.is_empty() {
|
||||||
|
app.error_message = Some("Реакции недоступны для этого сообщения".to_string());
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
} else {
|
||||||
|
app.enter_reaction_picker_mode(message_id, reactions);
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
app.error_message = Some(format!("Ошибка загрузки реакций: {}", e));
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
app.error_message = Some("Таймаут загрузки реакций".to_string());
|
||||||
|
app.status_message = None;
|
||||||
|
app.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -756,3 +875,91 @@ fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
|||||||
|
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Копирует текст в системный буфер обмена
|
||||||
|
fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||||
|
use arboard::Clipboard;
|
||||||
|
|
||||||
|
let mut clipboard = Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||||||
|
clipboard.set_text(text).map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Форматирует сообщение для копирования с контекстом
|
||||||
|
fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
// Добавляем forward контекст если есть
|
||||||
|
if let Some(forward) = &msg.forward_from {
|
||||||
|
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем reply контекст если есть
|
||||||
|
if let Some(reply) = &msg.reply_to {
|
||||||
|
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем основной текст с markdown форматированием
|
||||||
|
result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities));
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Конвертирует текст с entities в markdown
|
||||||
|
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
|
||||||
|
use tdlib_rs::enums::TextEntityType;
|
||||||
|
|
||||||
|
if entities.is_empty() {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём вектор символов для работы с unicode
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < chars.len() {
|
||||||
|
// Ищем entity, который начинается в текущей позиции
|
||||||
|
let mut entity_found = false;
|
||||||
|
|
||||||
|
for entity in entities {
|
||||||
|
if entity.offset as usize == i {
|
||||||
|
entity_found = true;
|
||||||
|
let end = (entity.offset + entity.length) as usize;
|
||||||
|
let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
|
||||||
|
|
||||||
|
// Применяем форматирование в зависимости от типа
|
||||||
|
let formatted = match &entity.r#type {
|
||||||
|
TextEntityType::Bold => format!("**{}**", entity_text),
|
||||||
|
TextEntityType::Italic => format!("*{}*", entity_text),
|
||||||
|
TextEntityType::Underline => format!("__{}__", entity_text),
|
||||||
|
TextEntityType::Strikethrough => format!("~~{}~~", entity_text),
|
||||||
|
TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => {
|
||||||
|
format!("`{}`", entity_text)
|
||||||
|
}
|
||||||
|
TextEntityType::TextUrl(url_info) => {
|
||||||
|
format!("[{}]({})", entity_text, url_info.url)
|
||||||
|
}
|
||||||
|
TextEntityType::Url => format!("<{}>", entity_text),
|
||||||
|
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||||||
|
format!("@{}", entity_text.trim_start_matches('@'))
|
||||||
|
}
|
||||||
|
TextEntityType::Spoiler => format!("||{}||", entity_text),
|
||||||
|
_ => entity_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push_str(&formatted);
|
||||||
|
i = end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !entity_found {
|
||||||
|
result.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ pub struct ForwardInfo {
|
|||||||
pub date: i32,
|
pub date: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Информация о реакции на сообщение
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReactionInfo {
|
||||||
|
/// Эмодзи реакции (например, "👍")
|
||||||
|
pub emoji: String,
|
||||||
|
/// Количество людей, поставивших эту реакцию
|
||||||
|
pub count: i32,
|
||||||
|
/// Поставил ли текущий пользователь эту реакцию
|
||||||
|
pub is_chosen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MessageInfo {
|
pub struct MessageInfo {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -159,6 +170,8 @@ pub struct MessageInfo {
|
|||||||
pub reply_to: Option<ReplyInfo>,
|
pub reply_to: Option<ReplyInfo>,
|
||||||
/// Информация о forward (если сообщение переслано)
|
/// Информация о forward (если сообщение переслано)
|
||||||
pub forward_from: Option<ForwardInfo>,
|
pub forward_from: Option<ForwardInfo>,
|
||||||
|
/// Реакции на сообщение
|
||||||
|
pub reactions: Vec<ReactionInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -623,6 +636,37 @@ impl TdClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Update::MessageInteractionInfo(update) => {
|
||||||
|
// Обновляем реакции в текущем открытом чате
|
||||||
|
if Some(update.chat_id) == self.current_chat_id {
|
||||||
|
if let Some(msg) = self.current_chat_messages.iter_mut().find(|m| m.id == update.message_id) {
|
||||||
|
// Извлекаем реакции из interaction_info
|
||||||
|
msg.reactions = update
|
||||||
|
.interaction_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|info| info.reactions.as_ref())
|
||||||
|
.map(|reactions| {
|
||||||
|
reactions
|
||||||
|
.reactions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|reaction| {
|
||||||
|
let emoji = match &reaction.r#type {
|
||||||
|
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||||||
|
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ReactionInfo {
|
||||||
|
emoji,
|
||||||
|
count: reaction.total_count,
|
||||||
|
is_chosen: reaction.is_chosen,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -789,6 +833,9 @@ impl TdClient {
|
|||||||
// Извлекаем информацию о forward
|
// Извлекаем информацию о forward
|
||||||
let forward_from = self.extract_forward_info(message);
|
let forward_from = self.extract_forward_info(message);
|
||||||
|
|
||||||
|
// Извлекаем реакции
|
||||||
|
let reactions = self.extract_reactions(message);
|
||||||
|
|
||||||
MessageInfo {
|
MessageInfo {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
sender_name,
|
sender_name,
|
||||||
@@ -803,6 +850,7 @@ impl TdClient {
|
|||||||
can_be_deleted_for_all_users: message.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: message.can_be_deleted_for_all_users,
|
||||||
reply_to,
|
reply_to,
|
||||||
forward_from,
|
forward_from,
|
||||||
|
reactions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,6 +907,34 @@ impl TdClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Извлекает информацию о реакциях из сообщения
|
||||||
|
fn extract_reactions(&self, message: &TdMessage) -> Vec<ReactionInfo> {
|
||||||
|
message
|
||||||
|
.interaction_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|info| info.reactions.as_ref())
|
||||||
|
.map(|reactions| {
|
||||||
|
reactions
|
||||||
|
.reactions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|reaction| {
|
||||||
|
// Извлекаем эмодзи из ReactionType
|
||||||
|
let emoji = match &reaction.r#type {
|
||||||
|
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||||||
|
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ReactionInfo {
|
||||||
|
emoji,
|
||||||
|
count: reaction.total_count,
|
||||||
|
is_chosen: reaction.is_chosen,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Получает имя отправителя из MessageOrigin
|
/// Получает имя отправителя из MessageOrigin
|
||||||
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
|
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
|
||||||
use tdlib_rs::enums::MessageOrigin;
|
use tdlib_rs::enums::MessageOrigin;
|
||||||
@@ -1504,12 +1580,96 @@ impl TdClient {
|
|||||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||||
reply_to: reply_info,
|
reply_to: reply_info,
|
||||||
forward_from: None,
|
forward_from: None,
|
||||||
|
reactions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Получить доступные реакции для сообщения
|
||||||
|
pub async fn get_message_available_reactions(
|
||||||
|
&mut self,
|
||||||
|
chat_id: i64,
|
||||||
|
message_id: i64,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
use tdlib_rs::functions;
|
||||||
|
|
||||||
|
let result = functions::get_message_available_reactions(
|
||||||
|
chat_id,
|
||||||
|
message_id,
|
||||||
|
8, // row_size - количество реакций в ряду
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(tdlib_rs::enums::AvailableReactions::AvailableReactions(reactions)) => {
|
||||||
|
// Извлекаем эмодзи из доступных реакций
|
||||||
|
// Используем top_reactions (самые популярные реакции)
|
||||||
|
let mut emojis: Vec<String> = reactions
|
||||||
|
.top_reactions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|reaction| {
|
||||||
|
if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type {
|
||||||
|
Some(e.emoji.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Если top_reactions пустой, используем popular_reactions
|
||||||
|
if emojis.is_empty() {
|
||||||
|
emojis = reactions
|
||||||
|
.popular_reactions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|reaction| {
|
||||||
|
if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type {
|
||||||
|
Some(e.emoji.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(emojis)
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Ошибка получения реакций: {:?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Добавить реакцию на сообщение (или убрать, если уже поставлена)
|
||||||
|
pub async fn toggle_reaction(
|
||||||
|
&mut self,
|
||||||
|
chat_id: i64,
|
||||||
|
message_id: i64,
|
||||||
|
emoji: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use tdlib_rs::functions;
|
||||||
|
use tdlib_rs::types::ReactionTypeEmoji;
|
||||||
|
use tdlib_rs::enums::ReactionType;
|
||||||
|
|
||||||
|
let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji });
|
||||||
|
|
||||||
|
let result = functions::add_message_reaction(
|
||||||
|
chat_id,
|
||||||
|
message_id,
|
||||||
|
reaction_type,
|
||||||
|
false, // is_big - обычная реакция (не "большая" анимация)
|
||||||
|
true, // update_recent_reactions - обновить список недавних реакций
|
||||||
|
self.client_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Ошибка добавления реакции: {:?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Редактирование текстового сообщения с поддержкой Markdown
|
/// Редактирование текстового сообщения с поддержкой Markdown
|
||||||
/// Устанавливает черновик для чата через TDLib API
|
/// Устанавливает черновик для чата через TDLib API
|
||||||
pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
|
pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
|
||||||
@@ -1616,6 +1776,7 @@ impl TdClient {
|
|||||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||||
reply_to: None, // При редактировании reply сохраняется из оригинала
|
reply_to: None, // При редактировании reply сохраняется из оригинала
|
||||||
forward_from: None, // При редактировании forward сохраняется из оригинала
|
forward_from: None, // При редактировании forward сохраняется из оригинала
|
||||||
|
reactions: Vec::new(), // При редактировании реакции сохраняются из оригинала
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)),
|
Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)),
|
||||||
|
|||||||
@@ -668,6 +668,58 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отображаем реакции под сообщением
|
||||||
|
if !msg.reactions.is_empty() {
|
||||||
|
let mut reaction_spans = vec![];
|
||||||
|
|
||||||
|
for reaction in &msg.reactions {
|
||||||
|
if !reaction_spans.is_empty() {
|
||||||
|
reaction_spans.push(Span::raw(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Свои реакции в рамках [emoji], чужие просто emoji
|
||||||
|
let reaction_text = if reaction.is_chosen {
|
||||||
|
if reaction.count > 1 {
|
||||||
|
format!("[{}] {}", reaction.emoji, reaction.count)
|
||||||
|
} else {
|
||||||
|
format!("[{}]", reaction.emoji)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if reaction.count > 1 {
|
||||||
|
format!("{} {}", reaction.emoji, reaction.count)
|
||||||
|
} else {
|
||||||
|
reaction.emoji.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = if reaction.is_chosen {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Gray)
|
||||||
|
};
|
||||||
|
|
||||||
|
reaction_spans.push(Span::styled(reaction_text, style));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выравниваем реакции в зависимости от типа сообщения
|
||||||
|
if msg.is_outgoing {
|
||||||
|
// Реакции справа для исходящих
|
||||||
|
let reactions_text: String = reaction_spans
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.content.as_ref())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
let reactions_len = reactions_text.chars().count();
|
||||||
|
let padding = content_width.saturating_sub(reactions_len + 1);
|
||||||
|
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||||
|
line_spans.extend(reaction_spans);
|
||||||
|
lines.push(Line::from(line_spans));
|
||||||
|
} else {
|
||||||
|
// Реакции слева для входящих
|
||||||
|
lines.push(Line::from(reaction_spans));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lines.is_empty() {
|
if lines.is_empty() {
|
||||||
@@ -734,10 +786,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
|
let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
|
||||||
|
|
||||||
let hint = match (can_edit, can_delete) {
|
let hint = match (can_edit, can_delete) {
|
||||||
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · d удал. · Esc",
|
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc",
|
||||||
(true, false) => "↑↓ · Enter ред. · r ответ · f переслть · Esc",
|
(true, false) => "↑↓ · Enter ред. · r ответ · f переслть · y копир. · Esc",
|
||||||
(false, true) => "↑↓ · r ответ · f переслать · d удалить · Esc",
|
(false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc",
|
||||||
(false, false) => "↑↓ · r ответить · f переслать · Esc",
|
(false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc",
|
||||||
};
|
};
|
||||||
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
||||||
} else if app.is_editing() {
|
} else if app.is_editing() {
|
||||||
@@ -827,6 +879,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
if app.is_confirm_delete_shown() {
|
if app.is_confirm_delete_shown() {
|
||||||
render_delete_confirm_modal(f, area);
|
render_delete_confirm_modal(f, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Модалка выбора реакции
|
||||||
|
if app.is_reaction_picker_mode() {
|
||||||
|
render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Рендерит режим поиска по сообщениям
|
/// Рендерит режим поиска по сообщениям
|
||||||
@@ -1136,3 +1193,78 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
|||||||
|
|
||||||
f.render_widget(modal, modal_area);
|
f.render_widget(modal, modal_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Рендерит модалку выбора реакции
|
||||||
|
fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) {
|
||||||
|
use ratatui::widgets::Clear;
|
||||||
|
|
||||||
|
// Размеры модалки (зависят от количества реакций)
|
||||||
|
let emojis_per_row = 8;
|
||||||
|
let rows = (available_reactions.len() + emojis_per_row - 1) / emojis_per_row;
|
||||||
|
let modal_width = 50u16;
|
||||||
|
let modal_height = (rows + 4) as u16; // +4 для заголовка, отступов и подсказки
|
||||||
|
|
||||||
|
// Центрируем модалку
|
||||||
|
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
|
||||||
|
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
|
||||||
|
|
||||||
|
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
|
||||||
|
|
||||||
|
// Очищаем область под модалкой
|
||||||
|
f.render_widget(Clear, modal_area);
|
||||||
|
|
||||||
|
// Формируем содержимое - сетка эмодзи
|
||||||
|
let mut text_lines = vec![Line::from("")]; // Пустая строка сверху
|
||||||
|
|
||||||
|
for row in 0..rows {
|
||||||
|
let mut row_spans = vec![Span::raw(" ")]; // Отступ слева
|
||||||
|
|
||||||
|
for col in 0..emojis_per_row {
|
||||||
|
let idx = row * emojis_per_row + col;
|
||||||
|
if idx >= available_reactions.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let emoji = &available_reactions[idx];
|
||||||
|
let is_selected = idx == selected_index;
|
||||||
|
|
||||||
|
let style = if is_selected {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
|
||||||
|
row_spans.push(Span::styled(format!(" {} ", emoji), style));
|
||||||
|
row_spans.push(Span::raw(" ")); // Пробел между эмодзи
|
||||||
|
}
|
||||||
|
|
||||||
|
text_lines.push(Line::from(row_spans));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем пустую строку и подсказку
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(vec![
|
||||||
|
Span::styled(" [←/→/↑/↓] ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw("Выбор "),
|
||||||
|
Span::styled(" [Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw("Добавить "),
|
||||||
|
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw("Отмена"),
|
||||||
|
]));
|
||||||
|
|
||||||
|
let modal = Paragraph::new(text_lines)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Yellow))
|
||||||
|
.title(" Выбери реакцию ")
|
||||||
|
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(modal, modal_area);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user