Save draft on app unload

This commit is contained in:
Alexander Zinchuk 2021-07-01 02:17:22 +03:00
parent 62c479976b
commit 1b568fc151
8 changed files with 165 additions and 110 deletions

View File

@ -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();

View File

@ -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

View File

@ -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}

View File

@ -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);
}; };

View File

@ -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 {

View File

@ -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]);
} }

View 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]);
}

View File

@ -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);
};
}