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 { GlobalActions } from '../../global/types';
@ -20,6 +22,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
import buildClassName from '../../util/buildClassName';
import useShowTransition from '../../hooks/useShowTransition';
import useBackgroundMode from '../../hooks/useBackgroundMode';
import useBeforeUnload from '../../hooks/useBeforeUnload';
import LeftColumn from '../left/LeftColumn';
import MiddleColumn from '../middle/MiddleColumn';
@ -131,26 +134,9 @@ const Main: FC<StateProps & DispatchProps> = ({
}
}, [animationLevel, isRightColumnShown]);
useBackgroundMode(() => {
const handleBlur = useCallback(() => {
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());
let index = 0;
@ -174,7 +160,11 @@ const Main: FC<StateProps & DispatchProps> = ({
index++;
}, NOTIFICATION_INTERVAL);
}, () => {
}, [updateIsOnline]);
const handleFocus = useCallback(() => {
updateIsOnline(true);
clearInterval(notificationInterval);
notificationInterval = undefined;
@ -183,7 +173,11 @@ const Main: FC<StateProps & DispatchProps> = ({
}
updateIcon(false);
});
}, [updateIsOnline]);
// Online status and browser tab indicators
useBackgroundMode(handleBlur, handleFocus);
useBeforeUnload(handleBlur);
function stopEvent(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();

View File

@ -777,11 +777,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
activeVoiceRecording && window.innerWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : lang('Message')
}
shouldSetFocus={isSymbolMenuOpen}
shouldSupressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
onUpdate={setHtml}
onSend={onSend}
onSupressedFocus={closeSymbolMenu}
onSuppressedFocus={closeSymbolMenu}
/>
{withScheduledButton && (
<Button

View File

@ -36,10 +36,10 @@ type OwnProps = {
html: string;
placeholder: string;
shouldSetFocus: boolean;
shouldSupressFocus?: boolean;
shouldSupressTextFormatter?: boolean;
shouldSuppressFocus?: boolean;
shouldSuppressTextFormatter?: boolean;
onUpdate: (html: string) => void;
onSupressedFocus?: () => void;
onSuppressedFocus?: () => void;
onSend: () => void;
};
@ -77,10 +77,10 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
html,
placeholder,
shouldSetFocus,
shouldSupressFocus,
shouldSupressTextFormatter,
shouldSuppressFocus,
shouldSuppressTextFormatter,
onUpdate,
onSupressedFocus,
onSuppressedFocus,
onSend,
currentChatId,
replyingToId,
@ -148,7 +148,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
const selectionRange = selection.getRangeAt(0);
const selectedText = selectionRange.toString().trim();
if (
shouldSupressTextFormatter
shouldSuppressTextFormatter
|| !isSelectionInsideInput(selectionRange, editableInputId || EDITABLE_INPUT_ID)
|| !selectedText
|| parseEmojiOnlyString(selectedText)
@ -344,27 +344,27 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
useEffect(() => {
const input = inputRef.current!;
function supressFocus() {
function suppressFocus() {
input.blur();
}
if (shouldSupressFocus) {
input.addEventListener('focus', supressFocus);
if (shouldSuppressFocus) {
input.addEventListener('focus', suppressFocus);
}
return () => {
input.removeEventListener('focus', supressFocus);
input.removeEventListener('focus', suppressFocus);
};
}, [shouldSupressFocus]);
}, [shouldSuppressFocus]);
const className = buildClassName(
'form-control custom-scroll',
html.length > 0 && 'touched',
shouldSupressFocus && 'focus-disabled',
shouldSuppressFocus && 'focus-disabled',
);
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
ref={inputRef}
id={editableInputId || EDITABLE_INPUT_ID}

View File

@ -9,6 +9,8 @@ import { debounce } from '../../../../util/schedulers';
import focusEditableElement from '../../../../util/focusEditableElement';
import parseMessageInput from '../helpers/parseMessageInput';
import getMessageTextAsHtml from '../helpers/getMessageTextAsHtml';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
// Used to avoid running debounced callbacks when chat changes.
let currentChatId: number | undefined;
@ -90,18 +92,12 @@ export default (
}
}, [chatId, html, prevChatId, prevHtml, prevThreadId, runDebouncedForSaveDraft, threadId, updateDraft]);
// Subscribe and handle `window.blur`
useEffect(() => {
function handleBlur() {
if (chatId && threadId) {
updateDraft(chatId, threadId);
}
const handleBlur = useCallback(() => {
if (chatId && threadId) {
updateDraft(chatId, threadId);
}
window.addEventListener('blur', handleBlur);
return () => {
window.removeEventListener('blur', handleBlur);
};
}, [chatId, threadId, updateDraft]);
useBackgroundMode(handleBlur);
useBeforeUnload(handleBlur);
};

View File

@ -5,7 +5,7 @@ import {
import { GlobalState } from './types';
import { MAIN_THREAD_ID } from '../api/types';
import { onIdle, throttle } from '../util/schedulers';
import { onBeforeUnload, onIdle, throttle } from '../util/schedulers';
import {
DEBUG,
GLOBAL_STATE_CACHE_DISABLED,
@ -20,11 +20,12 @@ import { INITIAL_STATE } from './initial';
import { selectCurrentMessageList } from '../modules/selectors';
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() {
if (GLOBAL_STATE_CACHE_DISABLED) {
@ -32,29 +33,54 @@ export function initCache() {
}
addReducer('saveSession', () => {
isAllowed = true;
addCallback(updateCacheThrottled);
if (isCaching) {
return;
}
setupCaching();
});
addReducer('reset', () => {
isAllowed = false;
removeCallback(updateCacheThrottled);
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
if (!isCaching) {
return;
}
clearCaching();
});
}
export function loadCache(initialState: GlobalState) {
if (!GLOBAL_STATE_CACHE_DISABLED) {
if (hasStoredSession(true)) {
isAllowed = true;
addCallback(updateCacheThrottled);
return readCache(initialState);
} else {
isAllowed = false;
}
if (GLOBAL_STATE_CACHE_DISABLED) {
return undefined;
}
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) {
@ -94,46 +120,44 @@ function readCache(initialState: GlobalState) {
}
function updateCache() {
onIdle(() => {
if (!isAllowed) {
return;
}
if (!isCaching) {
return;
}
const global = getGlobal();
const global = getGlobal();
if (global.isLoggingOut) {
return;
}
if (global.isLoggingOut) {
return;
}
const reducedGlobal: GlobalState = {
...INITIAL_STATE,
...pick(global, [
'authState',
'authPhoneNumber',
'authRememberMe',
'authNearestCountry',
'currentUserId',
'contactList',
'topPeers',
'recentEmojis',
'emojiKeywords',
'push',
'shouldShowContextMenuHint',
]),
isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global),
chats: reduceChats(global),
messages: reduceMessages(global),
globalSearch: {
recentlyFoundChatIds: global.globalSearch.recentlyFoundChatIds,
},
settings: reduceSettings(global),
chatFolders: reduceChatFolders(global),
};
const reducedGlobal: GlobalState = {
...INITIAL_STATE,
...pick(global, [
'authState',
'authPhoneNumber',
'authRememberMe',
'authNearestCountry',
'currentUserId',
'contactList',
'topPeers',
'recentEmojis',
'emojiKeywords',
'push',
'shouldShowContextMenuHint',
]),
isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global),
chats: reduceChats(global),
messages: reduceMessages(global),
globalSearch: {
recentlyFoundChatIds: global.globalSearch.recentlyFoundChatIds,
},
settings: reduceSettings(global),
chatFolders: reduceChatFolders(global),
};
const json = JSON.stringify(reducedGlobal);
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
});
const json = JSON.stringify(reducedGlobal);
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
}
function reduceShowChatInfo(global: GlobalState): boolean {

View File

@ -1,20 +1,30 @@
import { useEffect } from '../lib/teact/teact';
export default function useBackgroundMode(
onBlur: AnyToVoidFunction,
onFocus: AnyToVoidFunction,
onBlur?: AnyToVoidFunction,
onFocus?: AnyToVoidFunction,
) {
useEffect(() => {
if (!document.hasFocus()) {
if (onBlur && !document.hasFocus()) {
onBlur();
}
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
if (onBlur) {
window.addEventListener('blur', onBlur);
}
if (onFocus) {
window.addEventListener('focus', onFocus);
}
return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
if (onFocus) {
window.removeEventListener('focus', onFocus);
}
if (onBlur) {
window.removeEventListener('blur', onBlur);
}
};
}, [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) {
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);
};
}