feat(desktop): переход в комнату по уведомлению, выход без потери правок, Ctrl/Cmd+K

Три пункта спецификации клиента (docs/client-tauri.md §5), которые оставались
открытыми в обёртке:

- **Клик по уведомлению → переход в канал.** У `tauri-plugin-notification` на
  desktop нет колбэка действия, поэтому реализовано лучшее доступное поведение:
  уведомление, показанное при неактивном окне, запоминается, и когда приложение
  активируется в течение 10 секунд (окно получило фокус или macOS прислала
  `RunEvent::Reopen`), окно поднимается и открывается комната из уведомления —
  `glchat://guild/<id>/channel/<id>`, для личной беседы `glchat://dm/<id>`.
  Идентификаторы проверяются (только цифры), чтобы из текста уведомления нельзя
  было собрать произвольный deep link. Ограничение: ОС не сообщает, что
  активация вызвана именно кликом, поэтому переход сработает и при обычном
  возврате в приложение в эти 10 секунд — это описано в desktop/README.md;

- **Выход при несохранённых изменениях.** Клиент сообщает о правках командой
  `desktop_set_dirty`; закрытие окна и пункт «Выход» в трее перехватываются
  (`window::request_close_confirmation`), окно best-effort покачивается
  (`Window::set_position`, на Wayland молча пропускается), а решение принимает
  пользователь в диалоге клиента «Сохранить / Выйти без сохранения»; закрывает
  приложение команда `desktop_close_window`. Раньше выход из трея терял правки
  молча;

- **Ctrl/Cmd+K.** Комбинация регистрируется глобально (`shortcuts.rs`) и при
  неактивном окне поднимает окно и просит клиент открыть палитру команд
  (`WebMessage::OpenPalette`); повторы комбинаций с настраиваемыми шорткатами
  отсекаются по разобранной комбинации, иначе вторая регистрация падала бы.

Заодно: `updates.rs` получил тест, который разбирает манифест релиза типом
самого `tauri-plugin-updater` (`RemoteRelease`) — он ловит расхождение формата
(например, дату не в RFC 3339), из-за которого обновление молча не приходило бы.
Проверки: `make desktop-check` (fmt, clippy, 10 тестов).
This commit is contained in:
2026-09-22 23:17:27 +03:00
parent 498201b413
commit 72c5189e8f
9 changed files with 401 additions and 26 deletions
+34 -8
View File
@@ -12,6 +12,10 @@ use crate::bridge::{self, WebMessage};
use crate::settings::Settings;
use crate::state::AppState;
/// Палитра команд (docs/client-tauri.md §5): комбинация не настраивается —
/// это системный жест «быстрый переход», одинаковый во всех сборках.
const PALETTE_SHORTCUT: &str = "CommandOrControl+K";
/// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации.
pub fn on_shortcut<R: Runtime>(
app: &AppHandle<R>,
@@ -36,6 +40,12 @@ pub fn on_shortcut<R: Runtime>(
}
if parse(&settings.deafen_shortcut).as_ref() == Some(shortcut) {
bridge::send(app, &WebMessage::ToggleDeafen);
return;
}
// Быстрый переход: окно поднимаем и просим клиент открыть палитру команд.
if parse(PALETTE_SHORTCUT).as_ref() == Some(shortcut) {
crate::window::show_main(app);
bridge::send(app, &WebMessage::OpenPalette);
}
}
@@ -46,17 +56,33 @@ pub fn apply<R: Runtime>(app: &AppHandle<R>, settings: &Settings) -> Result<(),
.unregister_all()
.map_err(|error| format!("не удалось снять старые шорткаты: {error}"))?;
for spec in [
&settings.ptt_shortcut,
&settings.mute_shortcut,
&settings.deafen_shortcut,
] {
if spec.trim().is_empty() {
// Палитра команд регистрируется всегда и последней: если пользователь занял
// её комбинацию своим действием, второй раз её регистрировать нельзя —
// сработает то действие, чья проверка в `on_shortcut` стоит раньше.
let wanted = [
settings.ptt_shortcut.as_str(),
settings.mute_shortcut.as_str(),
settings.deafen_shortcut.as_str(),
PALETTE_SHORTCUT,
];
// Повторы отсекаются по разобранной комбинации: «Cmd+K» и «Command+K» —
// одна и та же клавиша, и вторая регистрация вернула бы ошибку.
let mut registered: Vec<Shortcut> = Vec::new();
for spec in wanted {
let trimmed = spec.trim();
if trimmed.is_empty() {
continue;
}
let Some(parsed) = parse(trimmed) else {
continue;
};
if registered.contains(&parsed) {
continue;
}
shortcuts
.register(spec.as_str())
.map_err(|error| format!("комбинация «{spec}» не зарегистрирована: {error}"))?;
.register(trimmed)
.map_err(|error| format!("комбинация «{trimmed}» не зарегистрирована: {error}"))?;
registered.push(parsed);
}
Ok(())
}