feat(desktop): обёртка Tauri 2 вокруг веб-клиента

Первый инкремент Фазы 6 (docs/client-tauri.md): окно с тем же веб-клиентом
(по умолчанию https://gl.mhspx.su, адрес настраивается), трей с меню и
индикатором непрочитанных, нативные уведомления, глобальный push-to-talk и
переключение микрофона/deafen, автозапуск, single-instance, сохранение
геометрии окна, deep links glchat://…, автообновление по подписанному
манифесту.

Связь страницы с обёрткой — через платформенный адаптер
(web/src/lib/platform.ts) и минимальный типизированный набор команд;
в веб-клиенте появились системные уведомления об упоминаниях и личных
сообщениях и переключатель в настройках.

В web/src/stores/gateway.ts вместе с вызовом уведомления лежат правки по
дефектам realtime (READY и сессии) из параллельной волны: файл коммитится
целиком по договорённости с координатором.
This commit is contained in:
2026-09-22 21:46:01 +03:00
parent 594a5c0af5
commit cffcec72b8
52 changed files with 8659 additions and 16 deletions
+79
View File
@@ -0,0 +1,79 @@
import { useEffect } from 'react';
import { unreadCount, useMessagesStore } from '@/stores/messages';
import { useVoiceConnectionStore } from '@/stores/voiceConnection';
import { platform, routeFromDeepLink } from '@/lib/platform';
import { router } from '@/router';
/**
* Мост desktop-обёртки (docs/client-tauri.md §2).
*
* Обёртка присылает события платформы (глобальный push-to-talk, mute/deafen,
* deep link, готовность обновления), а клиент отдаёт ей счётчик непрочитанных.
* В браузере компонент безвреден: реализация платформы там пустая.
*/
export function DesktopBridge() {
// События обёртки: глобальные шорткаты, deep links, обновления.
useEffect(
() =>
platform.onMessage((message) => {
const voice = useVoiceConnectionStore.getState();
switch (message.type) {
case 'ptt':
// Удержание клавиши: микрофон включается, пока она нажата.
voice.setPushToTalkActive(message.active);
break;
case 'toggleMic':
void voice.setMicEnabled(!voice.micEnabled);
break;
case 'toggleDeafen':
void voice.setDeafened(!voice.deafened);
break;
case 'deepLink': {
const route = routeFromDeepLink(message.url);
if (route === null) {
console.warn('[desktop] неизвестная ссылка', message.url);
break;
}
void router.navigate(route);
break;
}
case 'updateAvailable':
platform.notify({
title: 'Доступно обновление glchat',
body: `Версия ${message.version}. Установить можно из настроек приложения.`,
});
break;
case 'settings':
// Настройки обёртки клиенту не нужны: он читает только свои.
break;
}
}),
[],
);
// Обёртка отдаёт накопленные deep links после этого сигнала.
useEffect(() => {
platform.ready();
}, []);
// Счётчик непрочитанных для значка приложения и иконки в трее.
useEffect(() => {
let last = -1;
const sync = (): void => {
const channels = useMessagesStore.getState().channels;
let total = 0;
for (const record of Object.values(channels)) {
total += unreadCount(record);
}
if (total !== last) {
last = total;
platform.setBadge(total);
}
};
sync();
return useMessagesStore.subscribe(sync);
}, []);
return null;
}