From edfaa3f953648d5b9ec321630bf8da24fb39aecd Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Wed, 2 Oct 2019 16:00:33 +0800 Subject: [PATCH] Support multiple editor session and improve persistence. (#260) * Move the cursor to the end upon focusing text area * Allow multiple editor session and improve autosave * Fix data fetching * Fix mobile flash style --- web/package.json | 6 +- web/src/client.tsx | 7 +- web/src/components/App/index.tsx | 20 +- .../Common/Editor/BookSelector/index.scss | 4 - .../Common/Editor/BookSelector/index.tsx | 27 +- web/src/components/Common/Editor/Textarea.tsx | 25 +- web/src/components/Common/Editor/index.tsx | 53 ++- .../components/Common/EmailVerifyBanner.js | 126 ------ .../Common/EmailVerifyBanner.module.scss | 59 --- web/src/components/Common/Link/SafeNavLink.js | 44 -- web/src/components/Common/Link/internal.js | 57 --- web/src/components/Common/SystemMessage.scss | 2 +- web/src/components/Edit/Content.tsx | 26 +- web/src/components/Edit/index.tsx | 63 +-- web/src/components/Header/Normal.scss | 2 +- web/src/components/Header/Normal.tsx | 1 + web/src/components/Icons/Users.js | 3 +- web/src/components/LegacyEncrypt/index.js | 191 --------- web/src/components/LegacyJoin/JoinForm.js | 110 ----- web/src/components/LegacyJoin/index.js | 163 -------- web/src/components/LegacyLogin/LoginForm.js | 94 ----- .../LegacyLogin/OauthLoginButton.js | 57 --- web/src/components/LegacyLogin/index.js | 138 ------- web/src/components/LegacyLogin/module.scss | 166 -------- web/src/components/New/Content.tsx | 151 +++++++ web/src/components/New/index.tsx | 111 ++--- web/src/hocs/guestOnly.tsx | 8 +- web/src/hocs/userOnly.tsx | 25 +- .../Link/SafeLink.js => libs/editor.ts} | 26 +- web/src/libs/editor_test.ts | 49 +++ web/src/libs/fts/lexer_test.ts | 386 +++++++++--------- web/src/libs/hooks/editor.ts | 11 - web/src/libs/localStorage.ts | 10 +- web/src/store/editor/actions.ts | 35 +- web/src/store/editor/reducers.ts | 83 +++- web/src/store/editor/type.ts | 32 +- 36 files changed, 701 insertions(+), 1670 deletions(-) delete mode 100644 web/src/components/Common/EmailVerifyBanner.js delete mode 100644 web/src/components/Common/EmailVerifyBanner.module.scss delete mode 100644 web/src/components/Common/Link/SafeNavLink.js delete mode 100644 web/src/components/Common/Link/internal.js delete mode 100644 web/src/components/LegacyEncrypt/index.js delete mode 100644 web/src/components/LegacyJoin/JoinForm.js delete mode 100644 web/src/components/LegacyJoin/index.js delete mode 100644 web/src/components/LegacyLogin/LoginForm.js delete mode 100644 web/src/components/LegacyLogin/OauthLoginButton.js delete mode 100644 web/src/components/LegacyLogin/index.js delete mode 100644 web/src/components/LegacyLogin/module.scss create mode 100644 web/src/components/New/Content.tsx rename web/src/{components/Common/Link/SafeLink.js => libs/editor.ts} (62%) create mode 100644 web/src/libs/editor_test.ts diff --git a/web/package.json b/web/package.json index f69b208e..774a8701 100644 --- a/web/package.json +++ b/web/package.json @@ -5,9 +5,9 @@ "repository": "https://github.com/dnote/dnote", "main": "index", "scripts": { - "test": "./node_modules/karma/bin/karma start ./karma.conf.js --single-run", - "test:watch": "./node_modules/karma/bin/karma start ./karma.conf.js --watch", - "lint": "./node_modules/.bin/eslint ./src" + "test": "karma start ./karma.conf.js --single-run", + "test:watch": "karma start ./karma.conf.js --watch", + "lint": "eslint ./src/**/*.ts ./src/**/*.tsx" }, "author": "Monomax Software Pty Ltd", "license": "AGPL-3.0-or-later", diff --git a/web/src/client.tsx b/web/src/client.tsx index c39736d6..990cafb5 100644 --- a/web/src/client.tsx +++ b/web/src/client.tsx @@ -27,6 +27,7 @@ import { Provider } from 'react-redux'; import { debounce } from 'jslib/helpers/perf'; import App from './components/App'; import configureStore from './store'; +import { markPersisted } from './store/editor'; import { loadState, saveState } from './libs/localStorage'; import './libs/restoreScroll'; @@ -40,7 +41,11 @@ store.subscribe( saveState({ editor: state.editor }); - }, 1000) + + if (!state.editor.persisted) { + store.dispatch(markPersisted()); + } + }, 50) ); function renderApp() { diff --git a/web/src/components/App/index.tsx b/web/src/components/App/index.tsx index 6725d668..4136391f 100644 --- a/web/src/components/App/index.tsx +++ b/web/src/components/App/index.tsx @@ -56,15 +56,21 @@ interface Props extends RouteComponentProps {} function useFetchData() { const dispatch = useDispatch(); - useEffect(() => { - dispatch(getCurrentUser()).then(u => { - if (!u) { - return null; - } + const { user } = useSelector(state => { + return { + user: state.auth.user + }; + }); - return dispatch(getBooks()); - }); + useEffect(() => { + dispatch(getCurrentUser()); }, [dispatch]); + + useEffect(() => { + if (user.isFetched) { + dispatch(getBooks()); + } + }, [dispatch, user.isFetched]); } function hasLocationChanged(loc1: Location, loc2: Location) { diff --git a/web/src/components/Common/Editor/BookSelector/index.scss b/web/src/components/Common/Editor/BookSelector/index.scss index e06eac19..49f73d32 100644 --- a/web/src/components/Common/Editor/BookSelector/index.scss +++ b/web/src/components/Common/Editor/BookSelector/index.scss @@ -66,10 +66,6 @@ } } -.trigger-hidden { - visibility: hidden; -} - .book-label { margin-left: rem(8px); font-weight: 600; diff --git a/web/src/components/Common/Editor/BookSelector/index.tsx b/web/src/components/Common/Editor/BookSelector/index.tsx index a670e63d..6f59d238 100644 --- a/web/src/components/Common/Editor/BookSelector/index.tsx +++ b/web/src/components/Common/Editor/BookSelector/index.tsx @@ -26,11 +26,12 @@ import BookIcon from '../../../Icons/Book'; import CaretIcon from '../../../Icons/Caret'; import SearchInput from '../../SearchInput'; import { useDispatch, useSelector } from '../../../../store'; -import { updateBook } from '../../../../store/editor'; +import { updateBook, EditorSession } from '../../../../store/editor'; import OptionItem from './OptionItem'; import styles from './index.scss'; interface Props { + editor: EditorSession; wrapperClassName?: string; triggerClassName?: string; isReady: boolean; @@ -42,6 +43,7 @@ interface Props { } const BookSelector: React.SFC = ({ + editor, wrapperClassName, triggerClassName, isReady, @@ -50,10 +52,9 @@ const BookSelector: React.SFC = ({ setIsOpen, triggerRef }) => { - const { books, editor } = useSelector(state => { + const { books } = useSelector(state => { return { - books: state.books, - editor: state.editor + books: state.books }; }); const dispatch = useDispatch(); @@ -76,7 +77,13 @@ const BookSelector: React.SFC = ({ } function handleSelect(option) { - dispatch(updateBook({ label: option.label, uuid: option.value })); + dispatch( + updateBook({ + sessionKey: editor.sessionKey, + label: option.label, + uuid: option.value + }) + ); onAfterChange(); } @@ -105,10 +112,7 @@ const BookSelector: React.SFC = ({ className={classnames( styles.trigger, triggerClassName, - triggerProps.triggerClassName, - { - [styles['trigger-hidden']]: !isReady - } + triggerProps.triggerClassName )} onClick={() => { setIsOpen(!isOpen); @@ -116,17 +120,18 @@ const BookSelector: React.SFC = ({ aria-haspopup="menu" aria-expanded={ariaExpanded} aria-controls="book-filter" - disabled={books.isFetching} + disabled={!isReady} > - {currentLabel || 'Choose a book'} + {isReady ? currentLabel || 'Choose a book' : 'Loading...'} void; - doFlushContent: (string) => void; onSubmit: () => void; textareaRef: React.MutableRefObject; inputTimerRef: React.MutableRefObject; @@ -37,15 +36,16 @@ interface Props { } const Textarea: React.SFC = ({ + sessionKey, content, onChange, - doFlushContent, onSubmit, textareaRef, inputTimerRef, disabled }) => { const [contentFocused, setContentFocused] = useState(false); + const dispatch = useDispatch(); return (
@@ -65,7 +65,7 @@ const Textarea: React.SFC = ({ // eslint-disable-next-line no-param-reassign inputTimerRef.current = null; - doFlushContent(value); + dispatch(flushContent(sessionKey, value)); }, 1000); }} onFocus={() => { @@ -93,17 +93,4 @@ const Textarea: React.SFC = ({ ); }; -function mapStateToProps(state: AppState) { - return { - editor: state.editor - }; -} - -const mapDispatchToProps = { - doFlushContent: flushContent -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(Textarea); +export default Textarea; diff --git a/web/src/components/Common/Editor/index.tsx b/web/src/components/Common/Editor/index.tsx index 72a202f3..b36a4cdd 100644 --- a/web/src/components/Common/Editor/index.tsx +++ b/web/src/components/Common/Editor/index.tsx @@ -25,13 +25,30 @@ import { focusTextarea } from 'web/libs/dom'; import { getHomePath } from 'web/libs/paths'; import BooksSelector from './BookSelector'; import { useDispatch, useSelector } from '../../../store'; -import { flushContent, markDirty } from '../../../store/editor'; +import { + flushContent, + resetEditor, + EditorSession +} from '../../../store/editor'; import Textarea from './Textarea'; import Preview from './Preview'; import Button from '../Button'; import styles from './Editor.scss'; +function getContentCacheKey(editorSessionKey: string) { + return `editor.${editorSessionKey}.content`; +} + +function useEditorContent( + editor: EditorSession, + cacheKey: string +): [string, React.Dispatch] { + const cached = localStorage.getItem(cacheKey); + return useState(cached || editor.content); +} + interface Props { + editor: EditorSession; onSubmit: (param: { draftContent: string; draftBookUUID: string }) => void; isBusy: boolean; cancelPath?: Location; @@ -47,6 +64,7 @@ enum Mode { } const Editor: React.SFC = ({ + editor, onSubmit, isBusy, disabled, @@ -55,16 +73,16 @@ const Editor: React.SFC = ({ bookSelectorTriggerRef, cancelPath = getHomePath() }) => { - const { editor, books } = useSelector(state => { + const { books } = useSelector(state => { return { - editor: state.editor, books: state.books }; }); const dispatch = useDispatch(); const [bookSelectorOpen, setBookSelectorOpen] = useState(false); - const [content, setContent] = useState(editor.content); + const contentCacheKey = getContentCacheKey(editor.sessionKey); + const [content, setContent] = useEditorContent(editor, contentCacheKey); const [mode, setMode] = useState(Mode.write); const inputTimerRef = useRef(null); @@ -78,10 +96,11 @@ const Editor: React.SFC = ({ // eslint-disable-next-line no-param-reassign inputTimerRef.current = null; - dispatch(flushContent(content)); + dispatch(flushContent(editor.sessionKey, content)); } onSubmit({ draftContent: content, draftBookUUID: editor.bookUUID }); + localStorage.removeItem(contentCacheKey); } if (disabled) { @@ -100,13 +119,12 @@ const Editor: React.SFC = ({
{ - dispatch(markDirty()); - if (textareaRef.current) { focusTextarea(textareaRef.current); } @@ -148,10 +166,14 @@ const Editor: React.SFC = ({
{mode === Mode.write ? (