Files
glchat/desktop/src-tauri/src/bridge.rs
T
grendervill 72c5189e8f 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 тестов).
2026-09-22 23:17:27 +03:00

75 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Мост «Rust → веб-клиент».
//!
//! Обратное направление (страница → обёртка) идёт через типизированные команды
//! (`commands.rs`), а события платформы передаются вызовом единственной функции
//! `window.__glchatDesktop.receive(...)` в самой странице. Отдельного канала
//! событий нет намеренно: сообщений мало, а типы у них общие с `web/src/lib/platform.ts`.
use serde::Serialize;
use tauri::{AppHandle, Manager, Runtime};
use crate::settings::Settings;
use crate::state::AppState;
/// Метка главного окна (единственное окно приложения).
pub const WEB_WINDOW: &str = "main";
/// Сообщение веб-клиенту: то, что платформа сообщает странице.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WebMessage {
/// Нажат или отпущен глобальный push-to-talk.
Ptt { active: bool },
/// Переключить микрофон (глобальный шорткат).
ToggleMic,
/// Переключить «не слышу» (глобальный шорткат).
ToggleDeafen,
/// Открыть deep link: приглашение или сервер с комнатой.
DeepLink { url: String },
/// Настройки обёртки изменились (обновлена страница настроек).
Settings { settings: Settings },
/// Доступно обновление — клиент показывает уведомление в интерфейсе.
UpdateAvailable { version: String },
/// Обёртка перехватила закрытие окна: в клиенте есть несохранённые
/// изменения, он показывает тряску и диалог «Сохранить / Выйти без
/// сохранения» (docs/client-tauri.md §5).
CloseRequested,
/// Открыть палитру команд: глобальный Ctrl/Cmd+K при неактивном окне.
OpenPalette,
}
/// Отправляет сообщение в веб-клиент.
///
/// Если клиент ещё не загрузился, deep link ставится в очередь и будет отдан
/// после `desktop_ready` (docs/client-tauri.md §5). Остальные сообщения в этот
/// момент терять не страшно: они относятся к активному взаимодействию.
pub fn send<R: Runtime>(app: &AppHandle<R>, message: &WebMessage) -> bool {
let state = app.state::<AppState>();
if !state.web_ready() {
if let WebMessage::DeepLink { url } = message {
state.push_deep_link(url.clone());
}
return false;
}
dispatch(app, message)
}
/// Немедленная доставка без проверки готовности клиента.
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, message: &WebMessage) -> bool {
let Some(window) = app.get_webview_window(WEB_WINDOW) else {
return false;
};
let Ok(payload) = serde_json::to_string(message) else {
log::warn!("не удалось сериализовать сообщение веб-клиенту");
return false;
};
let script = format!("window.__glchatDesktop && window.__glchatDesktop.receive({payload});");
match window.eval(&script) {
Ok(()) => true,
Err(error) => {
log::debug!("веб-клиент не принял сообщение: {error}");
false
}
}
}