Улучшить звуки входящих личных сообщений
This commit is contained in:
parent
8768e142e3
commit
112ab4d5d5
@ -0,0 +1,31 @@
|
|||||||
|
## Краткое описание
|
||||||
|
|
||||||
|
Доработаны входящие уведомления для личных сообщений в сценарии, когда UI открыт, но страница скрыта на телефоне:
|
||||||
|
|
||||||
|
- для входящего DM при `document.visibilityState !== visible` UI пытается показать системное уведомление через `service worker`;
|
||||||
|
- добавлен `best effort` сигнал через `navigator.vibrate()`;
|
||||||
|
- добавлен короткий локальный звуковой сигнал через Web Audio, если аудио-контекст был ранее разблокирован пользовательским действием.
|
||||||
|
- для видимой активной страницы этот же сигнал теперь проигрывается на каждое новое входящее DM;
|
||||||
|
- для скрытой страницы звуковой сигнал сделан длиннее и заметнее.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
|
||||||
|
- открыть SHiNE в Chrome/Android и один раз взаимодействовать со страницей;
|
||||||
|
- свернуть браузер или увести вкладку в фон, не закрывая её полностью;
|
||||||
|
- отправить DM с другого аккаунта;
|
||||||
|
- при открытой видимой странице тоже отправить DM и убедиться, что короткий сигнал воспроизводится без системного уведомления в шторке;
|
||||||
|
- проверить, что:
|
||||||
|
- сообщение пришло в шторку как системное уведомление;
|
||||||
|
- при поддержке устройства есть вибрация;
|
||||||
|
- на части устройств/браузеров может прозвучать локальный сигнал;
|
||||||
|
- отдельно проверить, что при открытой видимой странице не появилось лишних дублей системного уведомления.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- скрытая, но живая страница стала заметнее реагировать на входящий DM;
|
||||||
|
- уведомление в фоне не зависит только от `new Notification(...)` из страницы;
|
||||||
|
- если браузер разрешает локальный аудио-сигнал, пользователь слышит короткое оповещение.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
`pending`
|
||||||
@ -184,6 +184,15 @@ Read-receipt пока остаются в legacy-формате:
|
|||||||
|
|
||||||
- `AckSessionDelivery`
|
- `AckSessionDelivery`
|
||||||
|
|
||||||
|
WebPush и локальные уведомления сейчас работают так:
|
||||||
|
|
||||||
|
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
|
||||||
|
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
|
||||||
|
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
|
||||||
|
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
|
||||||
|
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
|
||||||
|
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
|
||||||
|
|
||||||
## Правила UI
|
## Правила UI
|
||||||
|
|
||||||
UI сейчас работает так:
|
UI сейчас работает так:
|
||||||
@ -194,6 +203,8 @@ UI сейчас работает так:
|
|||||||
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||||
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||||
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||||
|
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
|
||||||
|
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
|
||||||
- не показывает и не принимает вложения.
|
- не показывает и не принимает вложения.
|
||||||
|
|
||||||
## Что обязательно помнить
|
## Что обязательно помнить
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.266
|
client.version=1.2.268
|
||||||
server.version=1.2.248
|
server.version=1.2.248
|
||||||
|
|||||||
@ -150,6 +150,8 @@ let uiUpdateReloadScheduled = false;
|
|||||||
let pwaUpdateCheckAttempted = false;
|
let pwaUpdateCheckAttempted = false;
|
||||||
let uiVersionCheckInFlight = false;
|
let uiVersionCheckInFlight = false;
|
||||||
let uiVersionPeriodicIntervalId = null;
|
let uiVersionPeriodicIntervalId = null;
|
||||||
|
let hiddenDmAudioContext = null;
|
||||||
|
let hiddenDmAudioUnlocked = false;
|
||||||
const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1';
|
const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1';
|
||||||
const GUEST_ALLOWED_PAGES = new Set([
|
const GUEST_ALLOWED_PAGES = new Set([
|
||||||
'start-view',
|
'start-view',
|
||||||
@ -172,6 +174,94 @@ initPwaInstallPromptHandling();
|
|||||||
initCallUiOverlay();
|
initCallUiOverlay();
|
||||||
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||||
|
|
||||||
|
async function unlockHiddenDmAudio() {
|
||||||
|
try {
|
||||||
|
const Ctx = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (!Ctx) return false;
|
||||||
|
if (!hiddenDmAudioContext) {
|
||||||
|
hiddenDmAudioContext = new Ctx();
|
||||||
|
}
|
||||||
|
if (hiddenDmAudioContext.state === 'suspended') {
|
||||||
|
await hiddenDmAudioContext.resume();
|
||||||
|
}
|
||||||
|
hiddenDmAudioUnlocked = hiddenDmAudioContext.state === 'running';
|
||||||
|
return hiddenDmAudioUnlocked;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playDmSignal({ extended = false } = {}) {
|
||||||
|
try {
|
||||||
|
if (!hiddenDmAudioUnlocked || !hiddenDmAudioContext) return false;
|
||||||
|
if (hiddenDmAudioContext.state === 'suspended') {
|
||||||
|
await hiddenDmAudioContext.resume();
|
||||||
|
}
|
||||||
|
if (hiddenDmAudioContext.state !== 'running') return false;
|
||||||
|
|
||||||
|
const now = hiddenDmAudioContext.currentTime;
|
||||||
|
const gain = hiddenDmAudioContext.createGain();
|
||||||
|
gain.connect(hiddenDmAudioContext.destination);
|
||||||
|
gain.gain.setValueAtTime(0.0001, now);
|
||||||
|
|
||||||
|
const pulse = (offsetSec, freqHz, durationSec, peakGain) => {
|
||||||
|
const osc = hiddenDmAudioContext.createOscillator();
|
||||||
|
osc.type = 'sine';
|
||||||
|
osc.frequency.setValueAtTime(freqHz, now + offsetSec);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(peakGain, now + offsetSec + 0.01);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, now + offsetSec + durationSec);
|
||||||
|
osc.start(now + offsetSec);
|
||||||
|
osc.stop(now + offsetSec + durationSec + 0.02);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extended) {
|
||||||
|
pulse(0, 880, 0.18, 0.032);
|
||||||
|
pulse(0.24, 1174, 0.2, 0.026);
|
||||||
|
pulse(0.52, 1567, 0.24, 0.02);
|
||||||
|
} else {
|
||||||
|
pulse(0, 1046, 0.12, 0.028);
|
||||||
|
pulse(0.17, 1318, 0.14, 0.022);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyHiddenIncomingMessage(fromLogin, text) {
|
||||||
|
const body = String(text || '').trim() || `Вам пришло сообщение от ${fromLogin}`;
|
||||||
|
const title = `Сообщение от ${fromLogin}`;
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker?.getRegistration?.();
|
||||||
|
if (registration?.showNotification) {
|
||||||
|
await registration.showNotification(title, {
|
||||||
|
body,
|
||||||
|
tag: `shine-hidden-dm-${String(fromLogin || '').trim().toLowerCase() || 'unknown'}`,
|
||||||
|
renotify: true,
|
||||||
|
data: {
|
||||||
|
kind: 'new_message',
|
||||||
|
fromLogin,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new Notification(title, { body });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore notification errors
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof navigator.vibrate === 'function') {
|
||||||
|
navigator.vibrate([140, 80, 220, 90, 180]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore vibration errors
|
||||||
|
}
|
||||||
|
|
||||||
|
void playDmSignal({ extended: true });
|
||||||
|
}
|
||||||
|
|
||||||
function ensureConnectionIndicatorEl() {
|
function ensureConnectionIndicatorEl() {
|
||||||
return document.getElementById('toolbar-connection-indicator');
|
return document.getElementById('toolbar-connection-indicator');
|
||||||
}
|
}
|
||||||
@ -960,10 +1050,16 @@ async function init() {
|
|||||||
if (added && isIncomingForCurrent) {
|
if (added && isIncomingForCurrent) {
|
||||||
shouldRefreshToolbarUnread = true;
|
shouldRefreshToolbarUnread = true;
|
||||||
}
|
}
|
||||||
if (added && isIncomingForCurrent && Notification.permission === 'granted' && !payload.backlog) {
|
if (added && isIncomingForCurrent && !payload.backlog) {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
void playDmSignal({ extended: false });
|
||||||
|
} else if (Notification.permission === 'granted') {
|
||||||
try {
|
try {
|
||||||
new Notification(`Сообщение от ${fromLogin}`, { body: text || '' });
|
void notifyHiddenIncomingMessage(fromLogin, text || '');
|
||||||
} catch {}
|
} catch {}
|
||||||
|
} else {
|
||||||
|
void playDmSignal({ extended: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (messageType === 3 || messageType === 4) {
|
} else if (messageType === 3 || messageType === 4) {
|
||||||
let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
|
let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
|
||||||
@ -1106,6 +1202,12 @@ async function init() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
window.addEventListener('popstate', renderApp);
|
window.addEventListener('popstate', renderApp);
|
||||||
|
document.addEventListener('pointerdown', () => {
|
||||||
|
void unlockHiddenDmAudio();
|
||||||
|
}, { passive: true });
|
||||||
|
document.addEventListener('keydown', () => {
|
||||||
|
void unlockHiddenDmAudio();
|
||||||
|
}, { passive: true });
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState !== 'visible') return;
|
if (document.visibilityState !== 'visible') return;
|
||||||
void checkConnectionHealth();
|
void checkConnectionHealth();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user