Save draft on app unload
This commit is contained in:
parent
62c479976b
commit
1b568fc151
@ -1,4 +1,6 @@
|
|||||||
import React, { FC, useEffect, memo } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
FC, useEffect, memo, useCallback,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
|
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { GlobalActions } from '../../global/types';
|
import { GlobalActions } from '../../global/types';
|
||||||
@ -20,6 +22,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
|
|||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||||
|
import useBeforeUnload from '../../hooks/useBeforeUnload';
|
||||||
|
|
||||||
import LeftColumn from '../left/LeftColumn';
|
import LeftColumn from '../left/LeftColumn';
|
||||||
import MiddleColumn from '../middle/MiddleColumn';
|
import MiddleColumn from '../middle/MiddleColumn';
|
||||||
@ -131,26 +134,9 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
}, [animationLevel, isRightColumnShown]);
|
}, [animationLevel, isRightColumnShown]);
|
||||||
|
|
||||||
useBackgroundMode(() => {
|
const handleBlur = useCallback(() => {
|
||||||
updateIsOnline(false);
|
updateIsOnline(false);
|
||||||
}, () => {
|
|
||||||
updateIsOnline(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleUnload() {
|
|
||||||
updateIsOnline(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', handleUnload);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeunload', handleUnload);
|
|
||||||
};
|
|
||||||
}, [updateIsOnline]);
|
|
||||||
|
|
||||||
// Browser tab indicators
|
|
||||||
useBackgroundMode(() => {
|
|
||||||
const initialUnread = selectCountNotMutedUnread(getGlobal());
|
const initialUnread = selectCountNotMutedUnread(getGlobal());
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
@ -174,7 +160,11 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
index++;
|
index++;
|
||||||
}, NOTIFICATION_INTERVAL);
|
}, NOTIFICATION_INTERVAL);
|
||||||
}, () => {
|
}, [updateIsOnline]);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
updateIsOnline(true);
|
||||||
|
|
||||||
clearInterval(notificationInterval);
|
clearInterval(notificationInterval);
|
||||||
notificationInterval = undefined;
|
notificationInterval = undefined;
|
||||||
|
|
||||||
@ -183,7 +173,11 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateIcon(false);
|
updateIcon(false);
|
||||||
});
|
}, [updateIsOnline]);
|
||||||
|
|
||||||
|
// Online status and browser tab indicators
|
||||||
|
useBackgroundMode(handleBlur, handleFocus);
|
||||||
|
useBeforeUnload(handleBlur);
|
||||||
|
|
||||||
function stopEvent(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function stopEvent(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -777,11 +777,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
activeVoiceRecording && window.innerWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : lang('Message')
|
activeVoiceRecording && window.innerWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : lang('Message')
|
||||||
}
|
}
|
||||||
shouldSetFocus={isSymbolMenuOpen}
|
shouldSetFocus={isSymbolMenuOpen}
|
||||||
shouldSupressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
|
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
|
||||||
shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
|
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
|
||||||
onUpdate={setHtml}
|
onUpdate={setHtml}
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
onSupressedFocus={closeSymbolMenu}
|
onSuppressedFocus={closeSymbolMenu}
|
||||||
/>
|
/>
|
||||||
{withScheduledButton && (
|
{withScheduledButton && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -36,10 +36,10 @@ type OwnProps = {
|
|||||||
html: string;
|
html: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
shouldSetFocus: boolean;
|
shouldSetFocus: boolean;
|
||||||
shouldSupressFocus?: boolean;
|
shouldSuppressFocus?: boolean;
|
||||||
shouldSupressTextFormatter?: boolean;
|
shouldSuppressTextFormatter?: boolean;
|
||||||
onUpdate: (html: string) => void;
|
onUpdate: (html: string) => void;
|
||||||
onSupressedFocus?: () => void;
|
onSuppressedFocus?: () => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,10 +77,10 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
html,
|
html,
|
||||||
placeholder,
|
placeholder,
|
||||||
shouldSetFocus,
|
shouldSetFocus,
|
||||||
shouldSupressFocus,
|
shouldSuppressFocus,
|
||||||
shouldSupressTextFormatter,
|
shouldSuppressTextFormatter,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onSupressedFocus,
|
onSuppressedFocus,
|
||||||
onSend,
|
onSend,
|
||||||
currentChatId,
|
currentChatId,
|
||||||
replyingToId,
|
replyingToId,
|
||||||
@ -148,7 +148,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const selectionRange = selection.getRangeAt(0);
|
const selectionRange = selection.getRangeAt(0);
|
||||||
const selectedText = selectionRange.toString().trim();
|
const selectedText = selectionRange.toString().trim();
|
||||||
if (
|
if (
|
||||||
shouldSupressTextFormatter
|
shouldSuppressTextFormatter
|
||||||
|| !isSelectionInsideInput(selectionRange, editableInputId || EDITABLE_INPUT_ID)
|
|| !isSelectionInsideInput(selectionRange, editableInputId || EDITABLE_INPUT_ID)
|
||||||
|| !selectedText
|
|| !selectedText
|
||||||
|| parseEmojiOnlyString(selectedText)
|
|| parseEmojiOnlyString(selectedText)
|
||||||
@ -344,27 +344,27 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const input = inputRef.current!;
|
const input = inputRef.current!;
|
||||||
|
|
||||||
function supressFocus() {
|
function suppressFocus() {
|
||||||
input.blur();
|
input.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSupressFocus) {
|
if (shouldSuppressFocus) {
|
||||||
input.addEventListener('focus', supressFocus);
|
input.addEventListener('focus', suppressFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
input.removeEventListener('focus', supressFocus);
|
input.removeEventListener('focus', suppressFocus);
|
||||||
};
|
};
|
||||||
}, [shouldSupressFocus]);
|
}, [shouldSuppressFocus]);
|
||||||
|
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'form-control custom-scroll',
|
'form-control custom-scroll',
|
||||||
html.length > 0 && 'touched',
|
html.length > 0 && 'touched',
|
||||||
shouldSupressFocus && 'focus-disabled',
|
shouldSuppressFocus && 'focus-disabled',
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} onClick={shouldSupressFocus ? onSupressedFocus : undefined} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div id={id} onClick={shouldSuppressFocus ? onSuppressedFocus : undefined} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<div
|
<div
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id={editableInputId || EDITABLE_INPUT_ID}
|
id={editableInputId || EDITABLE_INPUT_ID}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { debounce } from '../../../../util/schedulers';
|
|||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import parseMessageInput from '../helpers/parseMessageInput';
|
import parseMessageInput from '../helpers/parseMessageInput';
|
||||||
import getMessageTextAsHtml from '../helpers/getMessageTextAsHtml';
|
import getMessageTextAsHtml from '../helpers/getMessageTextAsHtml';
|
||||||
|
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||||
|
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
|
||||||
|
|
||||||
// Used to avoid running debounced callbacks when chat changes.
|
// Used to avoid running debounced callbacks when chat changes.
|
||||||
let currentChatId: number | undefined;
|
let currentChatId: number | undefined;
|
||||||
@ -90,18 +92,12 @@ export default (
|
|||||||
}
|
}
|
||||||
}, [chatId, html, prevChatId, prevHtml, prevThreadId, runDebouncedForSaveDraft, threadId, updateDraft]);
|
}, [chatId, html, prevChatId, prevHtml, prevThreadId, runDebouncedForSaveDraft, threadId, updateDraft]);
|
||||||
|
|
||||||
// Subscribe and handle `window.blur`
|
const handleBlur = useCallback(() => {
|
||||||
useEffect(() => {
|
if (chatId && threadId) {
|
||||||
function handleBlur() {
|
updateDraft(chatId, threadId);
|
||||||
if (chatId && threadId) {
|
|
||||||
updateDraft(chatId, threadId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('blur', handleBlur);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('blur', handleBlur);
|
|
||||||
};
|
|
||||||
}, [chatId, threadId, updateDraft]);
|
}, [chatId, threadId, updateDraft]);
|
||||||
|
|
||||||
|
useBackgroundMode(handleBlur);
|
||||||
|
useBeforeUnload(handleBlur);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
import { GlobalState } from './types';
|
import { GlobalState } from './types';
|
||||||
import { MAIN_THREAD_ID } from '../api/types';
|
import { MAIN_THREAD_ID } from '../api/types';
|
||||||
|
|
||||||
import { onIdle, throttle } from '../util/schedulers';
|
import { onBeforeUnload, onIdle, throttle } from '../util/schedulers';
|
||||||
import {
|
import {
|
||||||
DEBUG,
|
DEBUG,
|
||||||
GLOBAL_STATE_CACHE_DISABLED,
|
GLOBAL_STATE_CACHE_DISABLED,
|
||||||
@ -20,11 +20,12 @@ import { INITIAL_STATE } from './initial';
|
|||||||
import { selectCurrentMessageList } from '../modules/selectors';
|
import { selectCurrentMessageList } from '../modules/selectors';
|
||||||
import { hasStoredSession } from '../util/sessions';
|
import { hasStoredSession } from '../util/sessions';
|
||||||
|
|
||||||
const UPDATE_THROTTLE = 1000;
|
const UPDATE_THROTTLE = 5000;
|
||||||
|
|
||||||
const updateCacheThrottled = throttle(updateCache, UPDATE_THROTTLE, false);
|
const updateCacheThrottled = throttle(() => onIdle(updateCache), UPDATE_THROTTLE, false);
|
||||||
|
|
||||||
let isAllowed = false;
|
let isCaching = false;
|
||||||
|
let unsubscribeFromBeforeUnload: NoneToVoidFunction | undefined;
|
||||||
|
|
||||||
export function initCache() {
|
export function initCache() {
|
||||||
if (GLOBAL_STATE_CACHE_DISABLED) {
|
if (GLOBAL_STATE_CACHE_DISABLED) {
|
||||||
@ -32,29 +33,54 @@ export function initCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addReducer('saveSession', () => {
|
addReducer('saveSession', () => {
|
||||||
isAllowed = true;
|
if (isCaching) {
|
||||||
addCallback(updateCacheThrottled);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCaching();
|
||||||
});
|
});
|
||||||
|
|
||||||
addReducer('reset', () => {
|
addReducer('reset', () => {
|
||||||
isAllowed = false;
|
|
||||||
removeCallback(updateCacheThrottled);
|
|
||||||
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
||||||
|
|
||||||
|
if (!isCaching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCaching();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadCache(initialState: GlobalState) {
|
export function loadCache(initialState: GlobalState) {
|
||||||
if (!GLOBAL_STATE_CACHE_DISABLED) {
|
if (GLOBAL_STATE_CACHE_DISABLED) {
|
||||||
if (hasStoredSession(true)) {
|
return undefined;
|
||||||
isAllowed = true;
|
|
||||||
addCallback(updateCacheThrottled);
|
|
||||||
return readCache(initialState);
|
|
||||||
} else {
|
|
||||||
isAllowed = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
if (hasStoredSession(true)) {
|
||||||
|
setupCaching();
|
||||||
|
|
||||||
|
return readCache(initialState);
|
||||||
|
} else {
|
||||||
|
clearCaching();
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCaching() {
|
||||||
|
isCaching = true;
|
||||||
|
unsubscribeFromBeforeUnload = onBeforeUnload(updateCache, true);
|
||||||
|
window.addEventListener('blur', updateCache);
|
||||||
|
addCallback(updateCacheThrottled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCaching() {
|
||||||
|
isCaching = false;
|
||||||
|
removeCallback(updateCacheThrottled);
|
||||||
|
window.removeEventListener('blur', updateCache);
|
||||||
|
if (unsubscribeFromBeforeUnload) {
|
||||||
|
unsubscribeFromBeforeUnload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCache(initialState: GlobalState) {
|
function readCache(initialState: GlobalState) {
|
||||||
@ -94,46 +120,44 @@ function readCache(initialState: GlobalState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
onIdle(() => {
|
if (!isCaching) {
|
||||||
if (!isAllowed) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
|
|
||||||
if (global.isLoggingOut) {
|
if (global.isLoggingOut) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reducedGlobal: GlobalState = {
|
const reducedGlobal: GlobalState = {
|
||||||
...INITIAL_STATE,
|
...INITIAL_STATE,
|
||||||
...pick(global, [
|
...pick(global, [
|
||||||
'authState',
|
'authState',
|
||||||
'authPhoneNumber',
|
'authPhoneNumber',
|
||||||
'authRememberMe',
|
'authRememberMe',
|
||||||
'authNearestCountry',
|
'authNearestCountry',
|
||||||
'currentUserId',
|
'currentUserId',
|
||||||
'contactList',
|
'contactList',
|
||||||
'topPeers',
|
'topPeers',
|
||||||
'recentEmojis',
|
'recentEmojis',
|
||||||
'emojiKeywords',
|
'emojiKeywords',
|
||||||
'push',
|
'push',
|
||||||
'shouldShowContextMenuHint',
|
'shouldShowContextMenuHint',
|
||||||
]),
|
]),
|
||||||
isChatInfoShown: reduceShowChatInfo(global),
|
isChatInfoShown: reduceShowChatInfo(global),
|
||||||
users: reduceUsers(global),
|
users: reduceUsers(global),
|
||||||
chats: reduceChats(global),
|
chats: reduceChats(global),
|
||||||
messages: reduceMessages(global),
|
messages: reduceMessages(global),
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
recentlyFoundChatIds: global.globalSearch.recentlyFoundChatIds,
|
recentlyFoundChatIds: global.globalSearch.recentlyFoundChatIds,
|
||||||
},
|
},
|
||||||
settings: reduceSettings(global),
|
settings: reduceSettings(global),
|
||||||
chatFolders: reduceChatFolders(global),
|
chatFolders: reduceChatFolders(global),
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = JSON.stringify(reducedGlobal);
|
const json = JSON.stringify(reducedGlobal);
|
||||||
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
|
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduceShowChatInfo(global: GlobalState): boolean {
|
function reduceShowChatInfo(global: GlobalState): boolean {
|
||||||
|
|||||||
@ -1,20 +1,30 @@
|
|||||||
import { useEffect } from '../lib/teact/teact';
|
import { useEffect } from '../lib/teact/teact';
|
||||||
|
|
||||||
export default function useBackgroundMode(
|
export default function useBackgroundMode(
|
||||||
onBlur: AnyToVoidFunction,
|
onBlur?: AnyToVoidFunction,
|
||||||
onFocus: AnyToVoidFunction,
|
onFocus?: AnyToVoidFunction,
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!document.hasFocus()) {
|
if (onBlur && !document.hasFocus()) {
|
||||||
onBlur();
|
onBlur();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('blur', onBlur);
|
if (onBlur) {
|
||||||
window.addEventListener('focus', onFocus);
|
window.addEventListener('blur', onBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onFocus) {
|
||||||
|
window.addEventListener('focus', onFocus);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('focus', onFocus);
|
if (onFocus) {
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('focus', onFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onBlur) {
|
||||||
|
window.removeEventListener('blur', onBlur);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [onBlur, onFocus]);
|
}, [onBlur, onFocus]);
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/hooks/useBeforeUnload.ts
Normal file
9
src/hooks/useBeforeUnload.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
import { onBeforeUnload } from '../util/schedulers';
|
||||||
|
|
||||||
|
export default function useBeforeUnload(callback: AnyToVoidFunction) {
|
||||||
|
useEffect(() => {
|
||||||
|
return onBeforeUnload(callback);
|
||||||
|
}, [callback]);
|
||||||
|
}
|
||||||
@ -159,3 +159,25 @@ export function fastRaf(callback: NoneToVoidFunction, isPrimary = false) {
|
|||||||
export function fastPrimaryRaf(callback: NoneToVoidFunction) {
|
export function fastPrimaryRaf(callback: NoneToVoidFunction) {
|
||||||
fastRaf(callback, true);
|
fastRaf(callback, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let beforeUnloadCallbacks: NoneToVoidFunction[] | undefined;
|
||||||
|
|
||||||
|
export function onBeforeUnload(callback: NoneToVoidFunction, isLast = false) {
|
||||||
|
if (!beforeUnloadCallbacks) {
|
||||||
|
beforeUnloadCallbacks = [];
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
self.addEventListener('beforeunload', () => {
|
||||||
|
beforeUnloadCallbacks!.forEach((cb) => cb());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
beforeUnloadCallbacks.push(callback);
|
||||||
|
} else {
|
||||||
|
beforeUnloadCallbacks.unshift(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
beforeUnloadCallbacks = beforeUnloadCallbacks!.filter((cb) => cb !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user