diff --git a/src/App.tsx b/src/App.tsx index 46eb1b741..7be2422ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,12 +5,13 @@ import { getActions, withGlobal } from './global'; import type { GlobalState } from './global/types'; import type { UiLoaderPage } from './components/common/UiLoader'; -import { IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment'; +import { IS_INSTALL_PROMPT_SUPPORTED, IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment'; import { INACTIVE_MARKER, PAGE_TITLE } from './config'; import { selectTabState } from './global/selectors'; import { updateSizes } from './util/windowSize'; import { addActiveTabChangeListener } from './util/activeTabMonitor'; import { hasStoredSession } from './util/sessions'; +import { setupBeforeInstallPrompt } from './util/installPrompt'; import buildClassName from './util/buildClassName'; import { parseInitialLocationHash } from './util/routing'; import useFlag from './hooks/useFlag'; @@ -55,6 +56,12 @@ const App: FC = ({ const { isMobile } = useAppLayout(); const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android'; + useEffect(() => { + if (IS_INSTALL_PROMPT_SUPPORTED) { + setupBeforeInstallPrompt(); + } + }, []); + // Prevent drop on elements that do not accept it useEffect(() => { const body = document.body; diff --git a/src/bundles/calls.ts b/src/bundles/calls.ts index 41e31602b..34bf1d891 100644 --- a/src/bundles/calls.ts +++ b/src/bundles/calls.ts @@ -1,4 +1,11 @@ +import { IS_IOS, IS_SAFARI } from '../util/environment'; +import { initializeSoundsForSafari } from '../global/actions/ui/calls'; + export { default as GroupCall } from '../components/calls/group/GroupCall'; export { default as ActiveCallHeader } from '../components/calls/ActiveCallHeader'; export { default as PhoneCall } from '../components/calls/phone/PhoneCall'; export { default as RatePhoneCallModal } from '../components/calls/phone/RatePhoneCallModal'; + +if (IS_SAFARI || IS_IOS) { + document.addEventListener('click', initializeSoundsForSafari, { once: true }); +} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index dd42d4bb9..b39676662 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -32,6 +32,8 @@ import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { processDeepLink } from '../../util/deeplink'; import { parseInitialLocationHash, parseLocationHash } from '../../util/routing'; import { fastRaf } from '../../util/schedulers'; +import { Bundles, loadBundle } from '../../util/moduleLoader'; +import updateIcon from '../../util/updateIcon'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useBackgroundMode from '../../hooks/useBackgroundMode'; @@ -43,7 +45,7 @@ import useShowTransition from '../../hooks/useShowTransition'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import useInterval from '../../hooks/useInterval'; import useAppLayout from '../../hooks/useAppLayout'; -import updateIcon from '../../util/updateIcon'; +import useTimeout from '../../hooks/useTimeout'; import StickerSetModal from '../common/StickerSetModal.async'; import UnreadCount from '../common/UnreadCounter'; @@ -132,6 +134,7 @@ type StateProps = { }; const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min +const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec // eslint-disable-next-line @typescript-eslint/naming-convention let DEBUG_isLogged = false; @@ -222,6 +225,11 @@ const Main: FC = ({ console.log('>>> RENDER MAIN'); } + // Preload Calls bundle to initialize sounds for iOS + useTimeout(() => { + void loadBundle(Bundles.Calls); + }, CALL_BUNDLE_LOADING_DELAY_MS); + const { isDesktop } = useAppLayout(); useEffect(() => { if (!isLeftColumnOpen && !isMiddleColumnOpen && !isDesktop) { diff --git a/src/global/actions/apiUpdaters/calls.ts b/src/global/actions/apiUpdaters/calls.ts index 55be51936..d0821bab2 100644 --- a/src/global/actions/apiUpdaters/calls.ts +++ b/src/global/actions/apiUpdaters/calls.ts @@ -6,7 +6,7 @@ import { updateChat } from '../../reducers'; import { ARE_CALLS_SUPPORTED } from '../../../util/environment'; import { notifyAboutCall } from '../../../util/notifications'; import { selectGroupCall, selectPhoneCallUser } from '../../selectors/calls'; -import { checkNavigatorUserMediaPermissions, initializeSoundsForSafari } from '../ui/calls'; +import { checkNavigatorUserMediaPermissions, initializeSounds } from '../ui/calls'; import { onTickEnd } from '../../../util/schedulers'; import type { ActionReturnType } from '../../types'; import { updateTabState } from '../../reducers/tabs'; @@ -115,7 +115,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { }); }); - void initializeSoundsForSafari(); + initializeSounds(); void checkNavigatorUserMediaPermissions(global, actions, call.isVideo, getCurrentTabId()); global = { ...global, diff --git a/src/global/actions/ui/calls.ts b/src/global/actions/ui/calls.ts index 51a034303..6a5ca2bd6 100644 --- a/src/global/actions/ui/calls.ts +++ b/src/global/actions/ui/calls.ts @@ -26,16 +26,43 @@ import * as langProvider from '../../../util/langProvider'; import { updateTabState } from '../../reducers/tabs'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; -// Workaround for Safari not playing audio without user interaction +// This is a tiny MP3 file that is silent - retrieved from https://bigsoundbank.com and then modified +// eslint-disable-next-line max-len +const silentSound = 'data:audio/mpeg;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV'; + let audioElement: HTMLAudioElement | undefined; let audioContext: AudioContext | undefined; - let sounds: Record; -let initializationPromise: Promise | undefined = Promise.resolve(); -export const initializeSoundsForSafari = () => { - if (!initializationPromise) return Promise.resolve(); +// Workaround: this function is called once on the first user interaction. +// After that, it will be possible to play the notification on iOS without problems. +// https://rosswintle.uk/2019/01/skirting-the-ios-safari-audio-auto-play-policy-for-ui-sound-effects/ +export function initializeSoundsForSafari() { + initializeSounds(); + return Promise.all(Object.values(sounds).map((sound) => { + const prevSrc = sound.src; + sound.src = silentSound; + sound.muted = true; + sound.volume = 0.0001; + return sound.play() + .then(() => { + sound.pause(); + sound.volume = 1; + sound.currentTime = 0; + sound.muted = false; + + requestAnimationFrame(() => { + sound.src = prevSrc; + }); + }); + })); +} + +export function initializeSounds() { + if (sounds) { + return; + } const joinAudio = new Audio('./voicechat_join.mp3'); const connectingAudio = new Audio('./voicechat_connecting.mp3'); connectingAudio.loop = true; @@ -60,22 +87,7 @@ export const initializeSoundsForSafari = () => { busy: busyAudio, ringing: ringingAudio, }; - - initializationPromise = Promise.all(Object.values(sounds).map((sound) => { - sound.muted = true; - sound.volume = 0.0001; - return sound.play().then(() => { - sound.pause(); - sound.volume = 1; - sound.currentTime = 0; - sound.muted = false; - }); - })).then(() => { - initializationPromise = undefined; - }); - - return initializationPromise; -}; +} async function fetchGroupCall(global: T, groupCall: Partial) { const result = await callApi('getGroupCall', { @@ -258,7 +270,7 @@ addActionHandler('joinGroupCall', async (global, actions, payload): Promise { @@ -376,7 +384,7 @@ addActionHandler('requestCall', async (global, actions, payload): Promise return; } - await initializeSoundsForSafari(); + initializeSounds(); global = getGlobal(); void checkNavigatorUserMediaPermissions(global, actions, isVideo, tabId); diff --git a/src/index.tsx b/src/index.tsx index de83dcb30..ba8a92c3f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,8 +8,7 @@ import { getActions, getGlobal, } from './global'; import updateWebmanifest from './util/updateWebmanifest'; -import { setupBeforeInstallPrompt } from './util/installPrompt'; -import { IS_INSTALL_PROMPT_SUPPORTED, IS_MULTITAB_SUPPORTED } from './util/environment'; +import { IS_MULTITAB_SUPPORTED } from './util/environment'; import './global/init'; import { APP_VERSION, DEBUG, MULTITAB_LOCALSTORAGE_KEY } from './config'; @@ -59,10 +58,6 @@ async function init() { updateWebmanifest(); - if (IS_INSTALL_PROMPT_SUPPORTED) { - setupBeforeInstallPrompt(); - } - TeactDOM.render( , document.getElementById('root')!,