diff --git a/apps/mobile/android/app/src/main/assets/favicon.ico b/apps/mobile/android/app/src/main/assets/favicon.ico new file mode 100644 index 000000000..126cf3cd4 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/favicon.ico differ diff --git a/apps/mobile/src/components/EditorMenu/index.js b/apps/mobile/src/components/EditorMenu/index.js index 71a586e12..976c9cdee 100644 --- a/apps/mobile/src/components/EditorMenu/index.js +++ b/apps/mobile/src/components/EditorMenu/index.js @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useState, useEffect} from 'react'; import { ActivityIndicator, FlatList, @@ -21,36 +21,44 @@ import { SIZE, WEIGHT, } from '../../common/common'; -import {useTracked} from '../../provider'; +import {useTracked, ACTIONS} from '../../provider'; import {AnimatedSafeAreaView} from '../../views/Home'; import {VaultDialog} from '../VaultDialog'; +import {db} from '../../../App'; +import {w} from '../../utils/utils'; let tagsInputRef; let tagsList; -export const EditorMenu = ({ - close = () => {}, - hide, - update = () => {}, - updateProps = () => {}, - noteProps, - note, - timestamp, -}) => { +export const EditorMenu = ({updateProps = () => {}, timestamp}) => { const [state, dispatch] = useTracked(); const {colors} = state; - // Todo - - const changeColorScheme = () => {}; - /////// - - const [unlock, setUnlock] = useState(false); - const [vaultDialog, setVaultDialog] = useState(false); + const [noteProps, setNoteProps] = useState({ + pinned: false, + locked: false, + favorite: false, + tags: [], + colors: [], + }); const [focused, setFocused] = useState(false); + let tagToAdd = null; let backPressCount = 0; - _renderListItem = ({item, index}) => ( + function changeColorScheme(colors = COLOR_SCHEME, accent = ACCENT) { + let newColors = setColorScheme(colors, accent); + StatusBar.setBarStyle(newColors.night ? 'light-content' : 'dark-content'); + + dispatch({type: ACTIONS.THEME, colors: newColors}); + } + useEffect(() => { + if (timestamp) { + setNoteProps({...db.getNote(timestamp)}); + } + }, [timestamp]); + + const _renderListItem = item => ( { item.func(); @@ -61,9 +69,7 @@ export const EditorMenu = ({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end', - paddingHorizontal: 12, - paddingVertical: pv + 5, - paddingTop: index === 0 ? 5 : pv + 5, + paddingVertical: pv, }}> {item.name} @@ -89,7 +95,7 @@ export const EditorMenu = ({ {item.switch ? ( @@ -101,8 +107,8 @@ export const EditorMenu = ({ style={{ borderWidth: 2, borderColor: item.on ? colors.accent : colors.icon, - width: 23, - height: 23, + width: 22, + height: 22, justifyContent: 'center', alignItems: 'center', borderRadius: 100, @@ -116,29 +122,54 @@ export const EditorMenu = ({ ); - _renderTag = item => ( + _renderTag = tag => ( { + let oldProps = {...noteProps}; + + oldProps.tags.splice(oldProps.tags.indexOf(tag), 1); + db.addNote({ + dateCreated: timestamp, + content: noteProps.content, + title: noteProps.title, + tags: oldProps.tags, + }); + localRefresh(); + }} style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', - margin: 5, + margin: 1, paddingHorizontal: 5, paddingVertical: 2.5, - backgroundColor: colors.accent, - borderRadius: 5, }}> - {item} + + {tag.slice(0, 1)} + + {tag.slice(1)} ); + const localRefresh = () => { + if (!timestamp) return; + + let toAdd = db.getNote(timestamp); + + setNoteProps({...toAdd}); + }; + _renderColor = item => ( { @@ -149,25 +180,20 @@ export const EditorMenu = ({ props.colors.push(item); } - updateProps(props); + localRefresh(); }} style={{ flexDirection: 'row', - justifyContent: 'flex-start', + justifyContent: 'space-between', alignItems: 'center', - marginRight: 5, + marginBottom: 5, - borderWidth: 1.5, borderRadius: 100, - padding: 3, - borderColor: noteProps.colors.includes(item) - ? colors.pri - : colors.shade, }}> ); - _onSubmit = () => { + const _onSubmit = () => { if (!tagToAdd || tagToAdd === '#') return; let tag = tagToAdd; @@ -190,19 +216,31 @@ export const EditorMenu = ({ if (tag.includes(' ')) { tag = tag.replace(' ', '_'); } - let oldProps = {...noteProps}; - oldProps.tags.push(tag); + let oldProps = {...note}; + + if (oldProps.tags.includes(tag)) { + return; + } else { + oldProps.tags.push(tag); + } + tagsInputRef.setNativeProps({ text: '#', }); - updateProps(oldProps); + db.addNote({ + dateCreated: timestamp, + content: noteProps.content, + title: noteProps.title, + tags: oldProps.tags, + }); + localRefresh(); tagToAdd = ''; setTimeout(() => { - tagsInputRef.focus(); + //tagsInputRef.focus(); }, 300); }; - _onKeyPress = event => { + const _onKeyPress = event => { if (event.nativeEvent.key === 'Backspace') { if (backPressCount === 0 && !tagToAdd) { backPressCount = 1; @@ -212,13 +250,20 @@ export const EditorMenu = ({ if (backPressCount === 1 && !tagToAdd) { backPressCount = 0; - let tagInputValue = noteProps.tags[noteProps.tags.length - 1]; - let oldProps = {...noteProps}; - if (allTags.length === 1) return; + let tagInputValue = note.tags[note.tags.length - 1]; + let oldProps = {...note}; + if (oldProps.tags.length === 1) return; - oldProps.tags.splice(allTags.length - 1); + oldProps.tags.splice(oldProps.tags.length - 1); + + db.addNote({ + dateCreated: note.dateCreated, + content: note.content, + title: note.title, + tags: oldProps.tags, + }); + localRefresh(); - updateProps(oldProps); tagsInputRef.setNativeProps({ text: tagInputValue, }); @@ -236,11 +281,11 @@ export const EditorMenu = ({ duration={300} style={{ height: '100%', - opacity: hide ? 0 : 1, + opacity: 1, backgroundColor: colors.shade, }}> @@ -260,7 +304,7 @@ export const EditorMenu = ({ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', - width: '95%', + width: '100%', alignSelf: 'center', height: 50, }}> @@ -274,8 +318,8 @@ export const EditorMenu = ({ - + {[ { name: 'Dark Mode', icon: 'moon', @@ -302,10 +346,10 @@ export const EditorMenu = ({ name: 'Pinned', icon: 'tag', func: () => { - let props = {...noteProps}; - props.pinned = !noteProps.pinned; - - updateProps(props); + if (!timestamp) return; + db.pinItem(note.type, note.dateCreated); + localRefresh(); + dispatch({type: ACTIONS.PINNED}); }, close: false, check: true, @@ -316,10 +360,11 @@ export const EditorMenu = ({ name: 'Add to Favorites', icon: 'star', func: () => { - let props = {...noteProps}; - props.favorite = !noteProps.favorite; + if (!timestamp) return; - updateProps(props); + db.favoriteItem(note.type, note.dateCreated); + localRefresh(item.type); + dispatch({type: ACTIONS.FAVORITES}); }, close: false, check: true, @@ -355,20 +400,17 @@ export const EditorMenu = ({ icon: 'lock', func: () => { if (noteProps.locked) { - setUnlock(true); + //setUnlock(true); } else { - setUnlock(false); + // setUnlock(false); } - setVaultDialog(true); }, close: true, check: true, on: noteProps.locked, }, - ]} - keyExtractor={(item, index) => item.name} - renderItem={_renderListItem} - /> + ].map(_renderListItem)} + - (tagsList = ref)} - contentContainerStyle={{ + - {noteProps.tags.map(_renderTag)} + {noteProps && noteProps.tags + ? noteProps.tags.map(_renderTag) + : null} (tagsInputRef = ref)} placeholderTextColor={colors.icon} onFocus={() => { setFocused(true); }} selectionColor={colors.accent} - selectTextOnFocus={true} onBlur={() => { setFocused(false); }} @@ -444,10 +486,10 @@ export const EditorMenu = ({ tagToAdd = value; if (tagToAdd.length > 0) backPressCount = 0; }} - onKeyPress={_onKeyPress} onSubmitEditing={_onSubmit} + onKeyPress={_onKeyPress} /> - + - {[ 'red', @@ -501,7 +543,7 @@ export const EditorMenu = ({ 'orange', 'gray', ].map(_renderColor)} - + @@ -524,24 +566,6 @@ export const EditorMenu = ({ {} - - { - if (item) { - update(item); - } - let props = {...noteProps}; - props.locked = locked; - updateProps(props); - setVaultDialog(false); - setUnlock(false); - }} - note={note} - timestamp={timestamp} - perm={true} - openedToUnlock={unlock} - visible={vaultDialog} - /> ); diff --git a/apps/mobile/src/components/NoteItem/index.js b/apps/mobile/src/components/NoteItem/index.js index 1bfa83588..70116177b 100644 --- a/apps/mobile/src/components/NoteItem/index.js +++ b/apps/mobile/src/components/NoteItem/index.js @@ -1,5 +1,11 @@ import React from 'react'; -import {Dimensions, Text, TouchableOpacity, View} from 'react-native'; +import { + Dimensions, + Text, + TouchableOpacity, + View, + DeviceEventEmitter, +} from 'react-native'; import Icon from 'react-native-vector-icons/Feather'; import {DDS} from '../../../App'; import {ph, pv, SIZE, WEIGHT} from '../../common/common'; @@ -48,12 +54,10 @@ export default class NoteItem extends React.Component { let { colors, item, - width, customStyle, onLongPress, isTrash, pinned, - update, index, } = this.props; console.log('rendering', index); @@ -65,9 +69,7 @@ export default class NoteItem extends React.Component { justifyContent: 'flex-start', alignItems: 'center', flexDirection: 'row', - marginHorizontal: 12, - width: width, - + maxWidth: '100%', paddingRight: 6, alignSelf: 'center', borderBottomWidth: 1, @@ -110,9 +112,11 @@ export default class NoteItem extends React.Component { vaultDialog: true, }); } else { - NavigationService.navigate('Editor', { - note: item, - }); + DDS.isTab + ? DeviceEventEmitter.emit('loadNoteEvent', item) + : NavigationService.navigate('Editor', { + note: item, + }); } }} style={{ diff --git a/apps/mobile/src/components/NotesList/index.js b/apps/mobile/src/components/NotesList/index.js index afbb20758..c23c95f6b 100644 --- a/apps/mobile/src/components/NotesList/index.js +++ b/apps/mobile/src/components/NotesList/index.js @@ -8,6 +8,7 @@ import {NotesPlaceHolder} from '../ListPlaceholders'; import NoteItem from '../NoteItem'; import SelectionWrapper from '../SelectionWrapper'; import PullToRefresh from '../PullToRefresh'; +import {DDS} from '../../../App'; let sectionListRef; export const NotesList = ({ onScroll, @@ -25,7 +26,7 @@ export const NotesList = ({ { diff --git a/apps/mobile/src/components/SearchInput/index.js b/apps/mobile/src/components/SearchInput/index.js index 82dc54a2e..99204be8b 100644 --- a/apps/mobile/src/components/SearchInput/index.js +++ b/apps/mobile/src/components/SearchInput/index.js @@ -22,6 +22,7 @@ export const Search = props => { height: 60, justifyContent: 'center', marginTop: props.hide ? -65 : 0, + paddingHorizontal: 12, }}> { justifyContent: 'space-between', alignItems: 'center', paddingLeft: 12, - width: w - 24, + width: '100%', alignSelf: 'center', borderRadius: br, height: '90%', diff --git a/apps/mobile/src/components/SelectionHeader/index.js b/apps/mobile/src/components/SelectionHeader/index.js index 11ab0c79c..b15e48d81 100644 --- a/apps/mobile/src/components/SelectionHeader/index.js +++ b/apps/mobile/src/components/SelectionHeader/index.js @@ -31,17 +31,21 @@ export const SelectionHeader = ({navigation}) => { style={{ width: '100%', position: 'absolute', - height: selectionMode ? 50 + StatusBar.currentHeight : 0, + height: selectionMode + ? Platform.OS === 'android' + ? 50 + StatusBar.currentHeight + : 50 + : 0, opacity: selectionMode ? 1 : 0, backgroundColor: colors.bg, paddingTop: Platform.OS === 'ios' ? 0 : StatusBar.currentHeight, justifyContent: 'flex-end', zIndex: 11, + paddingHorizontal: 12, }}> { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - width: w - 24, - - marginHorizontal: 12, + width: '100%', + paddingHorizontal: 12, }}> - { dispatch({type: ACTIONS.SELECTED_ITEMS, item: item}); }} style={{ - width: 50, + width: '10%', height: 70, justifyContent: 'center', - alignItems: 'flex-start', + alignItems: 'center', display: selectionMode ? 'flex' : 'none', opacity: selectionMode ? 1 : 0, + paddingRight: 10, }}> { ) : null} - + {children} diff --git a/apps/mobile/src/components/header/index.js b/apps/mobile/src/components/header/index.js index 4d225fd28..14837e53c 100644 --- a/apps/mobile/src/components/header/index.js +++ b/apps/mobile/src/components/header/index.js @@ -7,6 +7,7 @@ import {useTracked} from '../../provider'; import NavigationService from '../../services/NavigationService'; import {SideMenuEvent} from '../../utils/utils'; import {moveNoteHideEvent} from '../DialogManager'; +import {DDS} from '../../../App'; let isOpen = false; export const Header = ({ heading, @@ -81,7 +82,7 @@ export const Header = ({ ) : ( undefined )} - {menu ? ( + {menu && !DDS.isTab ? ( { diff --git a/apps/mobile/src/views/Editor/index.js b/apps/mobile/src/views/Editor/index.js index 7d699a905..c0537b781 100755 --- a/apps/mobile/src/views/Editor/index.js +++ b/apps/mobile/src/views/Editor/index.js @@ -1,13 +1,14 @@ import React, {useEffect, useState} from 'react'; import { BackHandler, - Dimensions, KeyboardAvoidingView, Linking, Platform, StatusBar, TouchableOpacity, View, + DeviceEventEmitter, + ActivityIndicator, } from 'react-native'; import * as Animatable from 'react-native-animatable'; import Icon from 'react-native-vector-icons/Feather'; @@ -18,29 +19,77 @@ import { ActionSheetEvent, simpleDialogEvent, TEMPLATE_EXIT, + _recieveEvent, + _unSubscribeEvent, + TEMPLATE_EXIT_FULLSCREEN, } from '../../components/DialogManager'; -import {EditorMenu} from '../../components/EditorMenu'; import {useTracked, ACTIONS} from '../../provider'; -import {SideMenuEvent} from '../../utils/utils'; +import {SideMenuEvent, w} from '../../utils/utils'; import {AnimatedSafeAreaView} from '../Home'; let EditorWebView; let note = {}; -var timestamp = null; +let timestamp = null; var content = null; var title = null; let timer = null; -const Editor = ({navigation}) => { +let saveCounter = 0; +const Editor = ({navigation, noMenu}) => { // Global State const [state, dispatch] = useTracked(); const {colors} = state; + const [loading, setLoading] = useState(false); - // Local State - const [sidebar, setSidebar] = useState(DDS.isTab ? true : false); + let fullscreen = false; // FUNCTIONS const post = value => EditorWebView.postMessage(value); + useEffect(() => { + _recieveEvent('loadNoteEvent', loadNote); + + return () => { + _unSubscribeEvent('loadNoteEvent', loadNote); + }; + }, []); + + const loadNote = item => { + if (note && note.dateCreated) { + saveNote(true).then(() => { + dispatch({type: ACTIONS.NOTES}); + if (item && item.type === 'new') { + clearEditor(); + } else { + note = item; + updateEditor(); + } + }); + } else { + dispatch({type: ACTIONS.NOTES}); + if (item && item.type === 'new') { + clearEditor(); + } else { + note = item; + updateEditor(); + } + } + }; + + const clearEditor = () => { + timestamp = null; + title = null; + content = null; + note = {}; + saveCounter = 0; + + post('{}'); + post( + JSON.stringify({ + type: 'title', + value: null, + }), + ); + }; const onChange = data => { if (data !== '') { @@ -59,7 +108,7 @@ const Editor = ({navigation}) => { timer = null; onChange(evt.nativeEvent.data); timer = setTimeout(() => { - saveNote(true); + saveNote.call(this, true); }, 1000); } }; @@ -73,7 +122,10 @@ const Editor = ({navigation}) => { } }; - const saveNote = async (noteProps = {}, lockNote = true) => { + const saveNote = async (lockNote = true) => { + if (!title && !content) return; + if (title === '' && content.text === '') return; + if (!content) { content = { text: '', @@ -81,7 +133,7 @@ const Editor = ({navigation}) => { }; } - timestamp = await db.addNote({ + let dateCreated = await db.addNote({ title, content: { text: content.text, @@ -89,21 +141,60 @@ const Editor = ({navigation}) => { }, dateCreated: timestamp, }); + if (timestamp !== dateCreated) { + timestamp = dateCreated; + } - if (lockNote && db.getNote(timestamp).locked) { - db.lockNote(timestamp, 'password'); + if (content.text.length < 200 || saveCounter < 2) { + dispatch({ + type: ACTIONS.NOTES, + }); + } + saveCounter++; + if (timestamp) { + let lockednote = db.getNote(timestamp); + if (lockNote && lockednote.locked) { + await db.lockNote(timestamp, 'password'); + } } }; const onWebViewLoad = () => { - post(JSON.stringify(colors)); - if (navigation.state.params && navigation.state.params.note) { + if (noMenu) { + post( + JSON.stringify({ + type: 'nomenu', + value: true, + }), + ); + } else { + post( + JSON.stringify({ + type: 'nomenu', + value: false, + }), + ); + } + + if (navigation && navigation.state.params && navigation.state.params.note) { note = navigation.state.params.note; updateEditor(); + } else if (note && note.dateCreated) { + updateEditor(); } + + post(JSON.stringify(colors)); + + setTimeout(() => { + setLoading(false); + }, 1000); }; const updateEditor = () => { + title = note.title; + timestamp = note.dateCreated; + content = note.content; + saveCounter = 0; post(JSON.stringify(note.content.delta)); post( JSON.stringify({ @@ -111,12 +202,10 @@ const Editor = ({navigation}) => { value: note.title, }), ); - title = note.title; - timestamp = note.dateCreated; - content = note.content; }; const params = 'platform=' + Platform.OS; + const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/loader.html'; @@ -132,52 +221,100 @@ const Editor = ({navigation}) => { behavior={Platform.OS === 'ios' ? 'padding' : null} style={{ height: '100%', + width: '100%', + backgroundColor: 'transparent', }}> - { - simpleDialogEvent(TEMPLATE_EXIT('Editor')); - }} + + - + ) : null} + + + {noMenu ? null : ( + { + simpleDialogEvent(TEMPLATE_EXIT('Editor')); }} - name="chevron-left" - color={colors.icon} - size={SIZE.xxxl - 3} - /> - + style={{ + width: 60, + height: 50, + justifyContent: 'center', + alignItems: 'flex-start', + position: 'absolute', + left: 0, + top: 0, + marginTop: Platform.OS === 'ios' ? 0 : StatusBar.currentHeight, + paddingLeft: 12, + zIndex: 800, + }}> + + + )} { - DDS.isTab - ? setSidebar(!sidebar) - : ActionSheetEvent( - note, - true, - true, - ['Add to', 'Share', 'Export', 'Delete'], - ['Dark Mode', 'Add to Vault', 'Pin', 'Favorite'], - ); + if (fullscreen) { + DeviceEventEmitter.emit('closeFullScreenEditor'); + fullscreen = false; + post( + JSON.stringify({ + type: 'nomenu', + value: true, + }), + ); + } else { + DeviceEventEmitter.emit('showFullScreenEditor'); + fullscreen = true; + post( + JSON.stringify({ + type: 'nomenu', + value: false, + }), + ); + } + + return; + ActionSheetEvent( + note, + true, + true, + ['Add to', 'Share', 'Export', 'Delete'], + ['Dark Mode', 'Add to Vault', 'Pin', 'Favorite'], + ); }} style={{ - width: '12.5%', + width: 60, height: 50, justifyContent: 'center', alignItems: 'flex-end', @@ -186,7 +323,7 @@ const Editor = ({navigation}) => { top: 0, marginTop: Platform.OS === 'ios' ? 0 : StatusBar.currentHeight, paddingRight: 12, - zIndex: 999, + zIndex: 800, }}> @@ -196,7 +333,8 @@ const Editor = ({navigation}) => { onError={error => console.log(error)} onLoad={onWebViewLoad} javaScriptEnabled - onShouldStartLoadWithRequest={_onShouldStartLoadWithRequest} + injectedJavaScript={Platform.OS === 'ios' ? injectedJS : null} + //onShouldStartLoadWithRequest={_onShouldStartLoadWithRequest} renderLoading={() => ( { }} /> )} - cacheEnabled={true} + cacheMode="LOAD_NO_CACHE" + cacheEnabled={false} domStorageEnabled scrollEnabled={false} bounces={true} allowFileAccess={true} scalesPageToFit={true} + allowFileAccessFromFileURLs={true} + allowUniversalAccessFromFileURLs={true} originWhitelist={'*'} - injectedJavaScript={Platform.OS === 'ios' ? injectedJS : null} - source={ - Platform.OS === 'ios' - ? {uri: sourceUri} - : { - uri: 'file:///android_asset/texteditor.html', - baseUrl: 'baseUrl:"file:///android_asset/', - } - } + source={{ + uri: 'http://192.168.10.9:8080/texteditor.html', + }} style={{ height: '100%', maxHeight: '100%', @@ -236,24 +371,35 @@ const Editor = ({navigation}) => { // EFFECTS useEffect(() => { - let handleBack = BackHandler.addEventListener('hardwareBackPress', () => { - simpleDialogEvent(TEMPLATE_EXIT('Editor')); + let handleBack; + if (!noMenu) { + handleBack = BackHandler.addEventListener('hardwareBackPress', () => { + simpleDialogEvent(TEMPLATE_EXIT_FULLSCREEN()); + return true; + }); + } else { + if (handleBack) { + handleBack.remove(); + handleBack = null; + } + } - return true; - }); return () => { - handleBack.remove(); - handleBack = null; + if (handleBack) { + handleBack.remove(); + handleBack = null; + } title = null; content = null; timer = null; - timestamp = null; }; - }, []); + }, [noMenu]); useEffect(() => { - SideMenuEvent.disable(); + noMenu ? null : SideMenuEvent.disable(); + return () => { + if (noMenu) return; DDS.isTab ? SideMenuEvent.open() : null; SideMenuEvent.enable(); }; @@ -271,18 +417,11 @@ const Editor = ({navigation}) => { flex: 1, backgroundColor: colors.bg, height: '100%', - width: sidebar ? '70%' : '100%', + width: '100%', + flexDirection: 'row', + justifyContent: 'space-between', }}> {_renderEditor()} - - {DDS.isTab ? : null} - ); }; diff --git a/apps/mobile/src/views/Folders/index.js b/apps/mobile/src/views/Folders/index.js index 359dda3fc..b9974f021 100644 --- a/apps/mobile/src/views/Folders/index.js +++ b/apps/mobile/src/views/Folders/index.js @@ -15,6 +15,7 @@ import {ACTIONS, useTracked} from '../../provider'; import {slideLeft, slideRight} from '../../utils/animations'; import {w} from '../../utils/utils'; import {AddNotebookEvent} from '../../components/DialogManager'; +import {DDS} from '../../../App'; export const Folders = ({navigation}) => { const [state, dispatch] = useTracked(); @@ -171,7 +172,7 @@ export const Folders = ({navigation}) => { { contentContainerStyle={{ width: '100%', alignSelf: 'center', + minHeight: '100%', }} ListFooterComponent={ { bottomButtonText="Add a new note" bottomButtonOnPress={() => { dispatch({type: ACTIONS.NOTES}); - SideMenuEvent.close(); - SideMenuEvent.disable(); - NavigationService.navigate('Editor'); + + if (DDS.isTab) { + DeviceEventEmitter.emit('loadNoteEvent', {type: 'new'}); + } else { + SideMenuEvent.close(); + SideMenuEvent.disable(); + NavigationService.navigate('Editor'); + } }}> { const [state, dispatch] = useTracked();