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