diff --git a/src/App.tsx b/src/App.tsx index 6e1a19cb1..e72b669f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,42 @@ import { FC, useEffect } from './lib/teact/teact'; import React, { withGlobal } from './lib/teact/teactn'; -import { GlobalState } from './global/types'; +import { GlobalActions, GlobalState } from './global/types'; +import { INACTIVE_MARKER, PAGE_TITLE } from './config'; import { pick } from './util/iteratees'; import { updateSizes } from './util/windowSize'; +import { addActiveTabChangeListener } from './util/activeTabMonitor'; +import useFlag from './hooks/useFlag'; import Auth from './components/auth/Auth'; import UiLoader from './components/common/UiLoader'; import Main from './components/main/Main.async'; +import AppInactive from './components/main/AppInactive'; // import Test from './components/test/TestNoRedundancy'; type StateProps = Pick; +type DispatchProps = Pick; + +const App: FC = ({ authState, authIsSessionRemembered, disconnect }) => { + const [isInactive, markInactive] = useFlag(false); -const App: FC = ({ authState, authIsSessionRemembered }) => { useEffect(() => { updateSizes(); - }, []); + addActiveTabChangeListener(() => { + disconnect(); + document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`; + + markInactive(); + }); + }, [disconnect, markInactive]); // return ; + if (isInactive) { + return ; + } + if (authState) { switch (authState) { case 'authorizationStateWaitPhoneNumber': @@ -49,4 +66,5 @@ function renderMain() { export default withGlobal( (global): StateProps => pick(global, ['authState', 'authIsSessionRemembered']), + (setGlobal, actions): DispatchProps => pick(actions, ['disconnect']), )(App); diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index b708ff0c7..901b01723 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -95,6 +95,10 @@ export async function destroy() { await client.destroy(); } +export async function disconnect() { + await client.disconnect(); +} + export function getClient() { return client; } diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 493f05305..6ae1d87cd 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -1,4 +1,6 @@ -export { destroy, downloadMedia, fetchCurrentUser } from './client'; +export { + destroy, disconnect, downloadMedia, fetchCurrentUser, +} from './client'; export { provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr, diff --git a/src/assets/app-inactive.png b/src/assets/app-inactive.png new file mode 100644 index 000000000..f2878d7fe Binary files /dev/null and b/src/assets/app-inactive.png differ diff --git a/src/components/main/AppInactive.scss b/src/components/main/AppInactive.scss new file mode 100644 index 000000000..e2dad3c4f --- /dev/null +++ b/src/components/main/AppInactive.scss @@ -0,0 +1,32 @@ +#AppInactive { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .content { + max-width: 28rem; + margin: auto; + padding: 1.5rem; + text-align: center; + } + + .title { + margin-top: 1rem; + } + + .description { + color: var(--color-text-secondary); + font-size: 0.875rem; + } + + img { + width: 100%; + max-width: 20rem; + } + + .Button { + margin-top: 1rem; + } +} diff --git a/src/components/main/AppInactive.tsx b/src/components/main/AppInactive.tsx new file mode 100644 index 000000000..b9c68fbb9 --- /dev/null +++ b/src/components/main/AppInactive.tsx @@ -0,0 +1,32 @@ +import React, { FC } from '../../lib/teact/teact'; + +import Button from '../ui/Button'; + +import appInactivePath from '../../assets/app-inactive.png'; +import './AppInactive.scss'; + + +const AppInactive: FC = () => { + const handleReload = () => { + window.location.reload(); + }; + + return ( +
+
+ +

Such error, many tabs

+
+ Telegram supports only one active tab with the app. +
+ Please reload this page to continue using this tab or close it. +
+
+ +
+
+
+ ); +}; + +export default AppInactive; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 6bb0d2a8a..0a8e46ae5 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -5,7 +5,9 @@ import { GlobalActions } from '../../global/types'; import { ApiMessage } from '../../api/types'; import '../../modules/actions/all'; -import { ANIMATION_END_DELAY, DEBUG } from '../../config'; +import { + ANIMATION_END_DELAY, DEBUG, INACTIVE_MARKER, PAGE_TITLE, +} from '../../config'; import { pick } from '../../util/iteratees'; import { selectChatMessage, @@ -44,7 +46,6 @@ type StateProps = { type DispatchProps = Pick; -const APP_NAME = 'Telegram'; const ANIMATION_DURATION = 350; const NOTIFICATION_INTERVAL = 1000; @@ -122,6 +123,11 @@ const Main: FC = ({ clearInterval(notificationInterval); notificationInterval = window.setInterval(() => { + if (document.title.includes(INACTIVE_MARKER)) { + updateIcon(false); + return; + } + if (index % 2 === 0) { const newUnread = selectCountNotMutedUnread(getGlobal()) - initialUnread; if (newUnread > 0) { @@ -129,7 +135,7 @@ const Main: FC = ({ updateIcon(true); } } else { - document.title = APP_NAME; + document.title = PAGE_TITLE; updateIcon(false); } @@ -138,7 +144,11 @@ const Main: FC = ({ }, () => { clearInterval(notificationInterval); notificationInterval = undefined; - document.title = APP_NAME; + + if (!document.title.includes(INACTIVE_MARKER)) { + document.title = PAGE_TITLE; + } + updateIcon(false); }); diff --git a/src/config.ts b/src/config.ts index 8899f3b53..7c877233a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,6 @@ +export const PAGE_TITLE = 'Telegram'; +export const INACTIVE_MARKER = ' [Inactive]'; + export const APP_INFO = process.env.APP_INFO || 'Telegram T'; export const DEBUG = ( diff --git a/src/global/types.ts b/src/global/types.ts index 0ecba995a..43f13da82 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -383,7 +383,7 @@ export type GlobalState = { export type ActionTypes = ( // system - 'init' | 'reset' | 'initApi' | 'apiUpdate' | 'sync' | 'saveSession' | 'afterSync' | + 'init' | 'reset' | 'disconnect' | 'initApi' | 'apiUpdate' | 'sync' | 'saveSession' | 'afterSync' | 'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' | // ui 'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' | diff --git a/src/modules/actions/api/initial.ts b/src/modules/actions/api/initial.ts index 30e2eac41..287cc723f 100644 --- a/src/modules/actions/api/initial.ts +++ b/src/modules/actions/api/initial.ts @@ -126,6 +126,12 @@ addReducer('reset', () => { getDispatch().init(); }); +addReducer('disconnect', () => { + (async () => { + await callApi('disconnect'); + })(); +}); + addReducer('loadNearestCountry', (global) => { if (global.connectionState !== 'connectionStateReady') { return; diff --git a/src/util/activeTabMonitor.ts b/src/util/activeTabMonitor.ts new file mode 100644 index 000000000..96c5535ff --- /dev/null +++ b/src/util/activeTabMonitor.ts @@ -0,0 +1,19 @@ +const STORAGE_KEY = 'tt-active-tab'; +const INTERVAL = 2000; + +const tabKey = String(Date.now() + Math.random()); + +localStorage.setItem(STORAGE_KEY, tabKey); + +let callback: NoneToVoidFunction; + +const interval = window.setInterval(() => { + if (callback && localStorage.getItem(STORAGE_KEY) !== tabKey) { + callback(); + clearInterval(interval); + } +}, INTERVAL); + +export function addActiveTabChangeListener(_callback: NoneToVoidFunction) { + callback = _callback; +}