App Inactive: Support disconnecting when opening in another tab
This commit is contained in:
parent
baffa925d8
commit
987dfacaf5
24
src/App.tsx
24
src/App.tsx
@ -1,25 +1,42 @@
|
|||||||
import { FC, useEffect } from './lib/teact/teact';
|
import { FC, useEffect } from './lib/teact/teact';
|
||||||
import React, { withGlobal } from './lib/teact/teactn';
|
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 { pick } from './util/iteratees';
|
||||||
import { updateSizes } from './util/windowSize';
|
import { updateSizes } from './util/windowSize';
|
||||||
|
import { addActiveTabChangeListener } from './util/activeTabMonitor';
|
||||||
|
import useFlag from './hooks/useFlag';
|
||||||
|
|
||||||
import Auth from './components/auth/Auth';
|
import Auth from './components/auth/Auth';
|
||||||
import UiLoader from './components/common/UiLoader';
|
import UiLoader from './components/common/UiLoader';
|
||||||
import Main from './components/main/Main.async';
|
import Main from './components/main/Main.async';
|
||||||
|
import AppInactive from './components/main/AppInactive';
|
||||||
// import Test from './components/test/TestNoRedundancy';
|
// import Test from './components/test/TestNoRedundancy';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authState' | 'authIsSessionRemembered'>;
|
type StateProps = Pick<GlobalState, 'authState' | 'authIsSessionRemembered'>;
|
||||||
|
type DispatchProps = Pick<GlobalActions, 'disconnect'>;
|
||||||
|
|
||||||
|
const App: FC<StateProps & DispatchProps> = ({ authState, authIsSessionRemembered, disconnect }) => {
|
||||||
|
const [isInactive, markInactive] = useFlag(false);
|
||||||
|
|
||||||
const App: FC<StateProps> = ({ authState, authIsSessionRemembered }) => {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateSizes();
|
updateSizes();
|
||||||
}, []);
|
addActiveTabChangeListener(() => {
|
||||||
|
disconnect();
|
||||||
|
document.title = `${PAGE_TITLE}${INACTIVE_MARKER}`;
|
||||||
|
|
||||||
|
markInactive();
|
||||||
|
});
|
||||||
|
}, [disconnect, markInactive]);
|
||||||
|
|
||||||
// return <Test />;
|
// return <Test />;
|
||||||
|
|
||||||
|
if (isInactive) {
|
||||||
|
return <AppInactive />;
|
||||||
|
}
|
||||||
|
|
||||||
if (authState) {
|
if (authState) {
|
||||||
switch (authState) {
|
switch (authState) {
|
||||||
case 'authorizationStateWaitPhoneNumber':
|
case 'authorizationStateWaitPhoneNumber':
|
||||||
@ -49,4 +66,5 @@ function renderMain() {
|
|||||||
|
|
||||||
export default withGlobal(
|
export default withGlobal(
|
||||||
(global): StateProps => pick(global, ['authState', 'authIsSessionRemembered']),
|
(global): StateProps => pick(global, ['authState', 'authIsSessionRemembered']),
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, ['disconnect']),
|
||||||
)(App);
|
)(App);
|
||||||
|
|||||||
@ -95,6 +95,10 @@ export async function destroy() {
|
|||||||
await client.destroy();
|
await client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function disconnect() {
|
||||||
|
await client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
export function getClient() {
|
export function getClient() {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export { destroy, downloadMedia, fetchCurrentUser } from './client';
|
export {
|
||||||
|
destroy, disconnect, downloadMedia, fetchCurrentUser,
|
||||||
|
} from './client';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr,
|
provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr,
|
||||||
|
|||||||
BIN
src/assets/app-inactive.png
Normal file
BIN
src/assets/app-inactive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
32
src/components/main/AppInactive.scss
Normal file
32
src/components/main/AppInactive.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/components/main/AppInactive.tsx
Normal file
32
src/components/main/AppInactive.tsx
Normal file
@ -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 (
|
||||||
|
<div id="AppInactive">
|
||||||
|
<div className="content">
|
||||||
|
<img src={appInactivePath} alt="" />
|
||||||
|
<h3 className="title">Such error, many tabs</h3>
|
||||||
|
<div className="description">
|
||||||
|
Telegram supports only one active tab with the app.
|
||||||
|
<br />
|
||||||
|
Please reload this page to continue using this tab or close it.
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<Button isText ripple onClick={handleReload}>Reload app</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppInactive;
|
||||||
@ -5,7 +5,9 @@ import { GlobalActions } from '../../global/types';
|
|||||||
import { ApiMessage } from '../../api/types';
|
import { ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
import '../../modules/actions/all';
|
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 { pick } from '../../util/iteratees';
|
||||||
import {
|
import {
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
@ -44,7 +46,6 @@ type StateProps = {
|
|||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'loadAnimatedEmojis'>;
|
type DispatchProps = Pick<GlobalActions, 'loadAnimatedEmojis'>;
|
||||||
|
|
||||||
const APP_NAME = 'Telegram';
|
|
||||||
const ANIMATION_DURATION = 350;
|
const ANIMATION_DURATION = 350;
|
||||||
const NOTIFICATION_INTERVAL = 1000;
|
const NOTIFICATION_INTERVAL = 1000;
|
||||||
|
|
||||||
@ -122,6 +123,11 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
clearInterval(notificationInterval);
|
clearInterval(notificationInterval);
|
||||||
notificationInterval = window.setInterval(() => {
|
notificationInterval = window.setInterval(() => {
|
||||||
|
if (document.title.includes(INACTIVE_MARKER)) {
|
||||||
|
updateIcon(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (index % 2 === 0) {
|
if (index % 2 === 0) {
|
||||||
const newUnread = selectCountNotMutedUnread(getGlobal()) - initialUnread;
|
const newUnread = selectCountNotMutedUnread(getGlobal()) - initialUnread;
|
||||||
if (newUnread > 0) {
|
if (newUnread > 0) {
|
||||||
@ -129,7 +135,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
updateIcon(true);
|
updateIcon(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.title = APP_NAME;
|
document.title = PAGE_TITLE;
|
||||||
updateIcon(false);
|
updateIcon(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +144,11 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
}, () => {
|
}, () => {
|
||||||
clearInterval(notificationInterval);
|
clearInterval(notificationInterval);
|
||||||
notificationInterval = undefined;
|
notificationInterval = undefined;
|
||||||
document.title = APP_NAME;
|
|
||||||
|
if (!document.title.includes(INACTIVE_MARKER)) {
|
||||||
|
document.title = PAGE_TITLE;
|
||||||
|
}
|
||||||
|
|
||||||
updateIcon(false);
|
updateIcon(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 APP_INFO = process.env.APP_INFO || 'Telegram T';
|
||||||
|
|
||||||
export const DEBUG = (
|
export const DEBUG = (
|
||||||
|
|||||||
@ -383,7 +383,7 @@ export type GlobalState = {
|
|||||||
|
|
||||||
export type ActionTypes = (
|
export type ActionTypes = (
|
||||||
// system
|
// system
|
||||||
'init' | 'reset' | 'initApi' | 'apiUpdate' | 'sync' | 'saveSession' | 'afterSync' |
|
'init' | 'reset' | 'disconnect' | 'initApi' | 'apiUpdate' | 'sync' | 'saveSession' | 'afterSync' |
|
||||||
'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' |
|
'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' |
|
||||||
// ui
|
// ui
|
||||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
||||||
|
|||||||
@ -126,6 +126,12 @@ addReducer('reset', () => {
|
|||||||
getDispatch().init();
|
getDispatch().init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('disconnect', () => {
|
||||||
|
(async () => {
|
||||||
|
await callApi('disconnect');
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('loadNearestCountry', (global) => {
|
addReducer('loadNearestCountry', (global) => {
|
||||||
if (global.connectionState !== 'connectionStateReady') {
|
if (global.connectionState !== 'connectionStateReady') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
19
src/util/activeTabMonitor.ts
Normal file
19
src/util/activeTabMonitor.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user