move header inside webview

This commit is contained in:
Ammar Ahmed
2022-06-10 10:40:09 +05:00
parent cce06ed8b3
commit adca658f05
8 changed files with 424 additions and 102 deletions

View File

@@ -43,9 +43,9 @@ const Tabs = React.memo(
>
<Tab.Screen name="Notes" component={Home} />
<Tab.Screen name="Notebooks" component={Notebooks} />
<Tab.Screen name="Favorites" component={Favorites} />
<Tab.Screen name="Trash" component={Trash} />
<Tab.Screen name="Tags" component={Tags} />
<Tab.Screen options={{ lazy: true }} name="Favorites" component={Favorites} />
<Tab.Screen options={{ lazy: true }} name="Trash" component={Trash} />
<Tab.Screen options={{ lazy: true }} name="Tags" component={Tags} />
<Tab.Screen name="Settings" component={Settings} />
<Tab.Screen options={{ lazy: true }} name="TaggedNotes" component={TaggedNotes} />
<Tab.Screen options={{ lazy: true }} name="TopicNotes" component={TopicNotes} />

View File

@@ -209,7 +209,8 @@ const EditorHeader = ({ editor }) => {
flexDirection: 'row',
paddingHorizontal: 12,
height: 50,
justifyContent: 'space-between'
justifyContent: 'space-between',
display: 'none'
}}
>
<View

View File

@@ -1,11 +1,12 @@
import React from 'react';
import { Platform, View } from 'react-native';
import { Linking, Platform, View } from 'react-native';
import WebView from 'react-native-webview';
import { notesnook } from '../../../e2e/test.ids';
import { useUserStore } from '../../stores/use-user-store';
import EditorHeader from './header';
import { useEditor } from './tiptap/use-editor';
import { editorController } from './tiptap/utils';
import { useEditorEvents } from './tiptap/use-editor-events';
const sourceUri = '';
@@ -23,13 +24,20 @@ const Editor = React.memo(
() => {
const premiumUser = useUserStore(state => state.premium);
const editor = useEditor();
const onMessage = useEditorEvents(editor);
editorController.current = editor;
const onError = () => {
console.log('onError');
editorController.current?.setLoading(true);
setTimeout(() => editorController.current?.setLoading(false), 10);
};
const onShouldStartLoadWithRequest = request => {
Linking.openURL(request.url);
return false;
};
return editor.loading ? null : (
<>
<View
@@ -39,7 +47,6 @@ const Editor = React.memo(
flex: 1
}}
>
<EditorHeader editor={editor} />
<WebView
testID={notesnook.editor.id}
ref={editor.ref}
@@ -49,13 +56,16 @@ const Editor = React.memo(
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
javaScriptEnabled={true}
focusable={true}
setSupportMultipleWindows={false}
overScrollMode="never"
keyboardDisplayRequiresUserAction={false}
// onShouldStartLoadWithRequest={_onShouldStartLoadWithRequest}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
cacheMode="LOAD_DEFAULT"
cacheEnabled={true}
domStorageEnabled={true}
bounces={false}
setBuiltInZoomControls={false}
setDisplayZoomControls={false}
allowFileAccess={true}
scalesPageToFit={true}
renderLoading={() => <View />}
@@ -66,11 +76,11 @@ const Editor = React.memo(
allowUniversalAccessFromFileURLs={true}
originWhitelist={['*']}
source={{
uri: 'http://192.168.10.8:3000'
uri: 'http://192.168.10.3:3000'
}}
style={style}
autoManageStatusBarEnabled={false}
onMessage={editor.onMessage}
onMessage={onMessage}
/>
</View>
</>

View File

@@ -1,5 +1,6 @@
import { createRef, MutableRefObject, RefObject } from 'react';
import { Platform } from 'react-native';
import { EdgeInsets } from 'react-native-safe-area-context';
import WebView from 'react-native-webview';
import { db } from '../../../utils/database';
import { sleep } from '../../../utils/time';
@@ -8,6 +9,13 @@ import { getResponse, randId, textInput } from './utils';
type Action = { job: string; id: string };
export type Settings = {
readonly: boolean;
fullscreen: boolean;
deviceMode: string;
premium: boolean;
};
async function call(webview: RefObject<WebView | undefined>, action?: Action) {
if (!webview || !action) return;
setImmediate(() => webview.current?.injectJavaScript(action.job));
@@ -92,6 +100,29 @@ statusBar.current.set({date:"",saved:""});
);
};
setInsets = async (insets: EdgeInsets) => {
logger.info('setInsets', insets);
await call(
this.ref,
fn(`
if (typeof safeAreaController !== "undefined") {
safeAreaController.update(${JSON.stringify(insets)})
}
`)
);
};
setSettings = async (settings: Partial<Settings>) => {
await call(
this.ref,
fn(`
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(${JSON.stringify(settings)})
}
`)
);
};
setTags = async (note: Note | null | undefined) => {
if (!note) return;
let tags = note.tags

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import WebView from 'react-native-webview';
import { DDS } from '../../../services/device-detection';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../../../services/event-manager';
@@ -9,14 +10,11 @@ import { useTagStore } from '../../../stores/use-tag-store';
import { useThemeStore } from '../../../stores/use-theme-store';
import { db } from '../../../utils/database';
import { MMKV } from '../../../utils/database/mmkv';
import { eOnLoadNote, eOpenTagsDialog } from '../../../utils/events';
import filesystem from '../../../utils/filesystem';
import { eOnLoadNote } from '../../../utils/events';
import { tabBarRef } from '../../../utils/global-refs';
import { timeConverter } from '../../../utils/time';
import { AttachmentType } from '../../../utils/types';
import Commands from './commands';
import picker from './picker';
import { EditorState, Note, Content, AppState, SavePayload, EditorMessage } from './types';
import { AppState, Content, EditorState, Note, SavePayload } from './types';
import { defaultState, EditorEvents, isEditorLoaded, makeSessionId, post } from './utils';
export const useEditor = () => {
@@ -31,12 +29,17 @@ export const useEditor = () => {
const state = useRef<Partial<EditorState>>(defaultState);
const placeholderTip = useRef(TipManager.placeholderTip());
const tags = useTagStore(state => state.tags);
const insets = useSafeAreaInsets();
const postMessage = useCallback(
async (type: string, data: any) => await post(editorRef, type, data),
[]
);
useEffect(() => {
commands.setInsets(insets);
}, [insets]);
useEffect(() => {
commands.setTags(currentNote.current);
}, [tags]);
@@ -235,84 +238,6 @@ export const useEditor = () => {
};
}, []);
const onMessage = useCallback(
event => {
const data = event.nativeEvent.data;
let editorMessage = JSON.parse(data) as EditorMessage;
logger.info('editor', editorMessage.type);
if (editorMessage.sessionId !== sessionId && editorMessage.type !== EditorEvents.status) {
logger.error(
'editor',
'invalid session',
editorMessage.type,
sessionId,
editorMessage.sessionId
);
return;
}
switch (editorMessage.type) {
case EditorEvents.logger:
logger.info('editor-webview', editorMessage.value);
break;
case 'editor-event:content':
saveContent({
type: editorMessage.type,
content: editorMessage.value
});
break;
case 'editor-event:selection':
break;
case 'editor-event:title':
saveContent({
type: editorMessage.type,
title: editorMessage.value
});
break;
case 'editor-event:newtag':
if (!currentNote.current) return;
eSendEvent(eOpenTagsDialog, currentNote.current);
break;
case 'editor-event:tag':
if (editorMessage.value) {
if (!currentNote.current) return;
db.notes
//@ts-ignore
?.note(currentNote.current?.id)
.untag(editorMessage.value)
.then(async () => {
useTagStore.getState().setTags();
await commands.setTags(currentNote.current);
Navigation.queueRoutesForUpdate(
'ColoredNotes',
'Notes',
'TaggedNotes',
'TopicNotes',
'Tags'
);
});
}
break;
case 'editor-event:picker':
picker.pick();
break;
case 'editor-event:download-attachment':
filesystem.downloadAttachment(editorMessage.value?.hash, true);
break;
default:
console.log(
'unhandled event recieved from editor: ',
editorMessage.type,
editorMessage.value
);
break;
}
eSendEvent(editorMessage.type, editorMessage);
},
[sessionId]
);
const saveContent = useCallback(
({ title, content, type }: { title?: string; content?: string; type: string }) => {
if (type === EditorEvents.content) {
@@ -354,6 +279,7 @@ export const useEditor = () => {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
commands.setInsets(insets);
restoreEditorState();
}
}, [state, currentNote, loadNote]);
@@ -392,7 +318,6 @@ export const useEditor = () => {
}
return {
onMessage,
ref: editorRef,
onLoad,
commands,
@@ -403,6 +328,7 @@ export const useEditor = () => {
sessionId,
setSessionId,
note: currentNote,
onReady
onReady,
saveContent
};
};

View File

@@ -0,0 +1,310 @@
import { useCallback, useEffect, useRef } from 'react';
import { BackHandler, InteractionManager, NativeEventSubscription } from 'react-native';
import { Properties } from '../../../components/properties';
import { DDS } from '../../../services/device-detection';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from '../../../services/event-manager';
import Navigation from '../../../services/navigation';
import { useEditorStore } from '../../../stores/use-editor-store';
import { useSettingStore } from '../../../stores/use-setting-store';
import { useTagStore } from '../../../stores/use-tag-store';
import { useUserStore } from '../../../stores/use-user-store';
import umami from '../../../utils/analytics/index';
import { db } from '../../../utils/database';
import {
eClearEditor,
eCloseFullscreenEditor,
eOnLoadNote,
eOpenLoginDialog,
eOpenPremiumDialog,
eOpenPublishNoteDialog,
eOpenTagsDialog
} from '../../../utils/events';
import filesystem from '../../../utils/filesystem';
import { tabBarRef } from '../../../utils/global-refs';
import { NoteType } from '../../../utils/types';
import picker from './picker';
import { EditorMessage, useEditorType } from './types';
import { editorController, EditorEvents, editorState } from './utils';
export const EventTypes = {
selection: 'editor-event:selection',
content: 'editor-event:content',
title: 'editor-event:title',
scroll: 'editor-event:scroll',
history: 'editor-event:history',
newtag: 'editor-event:newtag',
tag: 'editor-event:tag',
filepicker: 'editor-event:picker',
download: 'editor-event:download-attachment',
logger: 'native:logger',
back: 'editor-event:back',
pro: 'editor-event:pro',
monograph: 'editor-event:monograph',
properties: 'editor-event:properties'
};
const publishNote = async () => {
const user = useUserStore.getState().user;
if (!user) {
ToastEvent.show({
heading: 'Login required',
message: 'Login to publish',
context: 'global',
func: () => {
eSendEvent(eOpenLoginDialog);
},
actionText: 'Login'
});
return;
}
if (!user.isEmailConfirmed) {
ToastEvent.show({
heading: 'Email not verified',
message: 'Please verify your email first.',
context: 'global'
});
return;
}
const currentNote = editorController.current?.note?.current;
if (currentNote?.id) {
let note = db.notes?.note(currentNote.id)?.data as NoteType;
if (note?.locked) {
ToastEvent.show({
heading: 'Locked notes cannot be published',
type: 'error',
context: 'global'
});
return;
}
if (editorState().isFocused) {
editorState().isFocused = true;
}
eSendEvent(eOpenPublishNoteDialog, note);
}
};
const showActionsheet = async () => {
const currentNote = editorController.current?.note?.current;
if (currentNote?.id) {
let note = db.notes?.note(currentNote.id)?.data as NoteType;
if (!note) {
ToastEvent.show({
heading: 'Start writing to create a new note',
type: 'success',
context: 'global'
});
return;
}
if (editorState().isFocused || editorState().isFocused) {
editorState().isFocused = true;
}
Properties.present(note, ['Dark Mode']);
}
};
export const useEditorEvents = (editor: useEditorType) => {
const deviceMode = useSettingStore(state => state.deviceMode);
const fullscreen = useSettingStore(state => state.fullscreen);
const handleBack = useRef<NativeEventSubscription>();
const currentEditingNote = useEditorStore(state => state.currentEditingNote);
const readonly = useEditorStore(state => state.readonly);
const isPremium = useUserStore(state => state.premium);
if (!editor) return null;
useEffect(() => {
editor.commands.setSettings({
deviceMode: deviceMode || 'mobile',
fullscreen: fullscreen,
premium: isPremium,
readonly: readonly
});
}, [currentEditingNote, fullscreen, isPremium, readonly]);
const onBackPress = useCallback(async () => {
setTimeout(async () => {
if (deviceMode !== 'mobile' && fullscreen) {
if (fullscreen) {
eSendEvent(eCloseFullscreenEditor);
}
return;
}
if (deviceMode === 'mobile') {
editorState().movedAway = true;
tabBarRef.current?.goToPage(0);
}
eSendEvent('historyEvent', {
undo: 0,
redo: 0
});
setImmediate(() => useEditorStore.getState().setCurrentlyEditingNote(null));
editorState().currentlyEditing = false;
editorController.current?.reset();
}, 1);
}, []);
const onHardwareBackPress = useCallback(() => {
if (editorState().currentlyEditing) {
onBackPress();
return true;
}
}, []);
const onLoadNote = useCallback(async () => {
InteractionManager.runAfterInteractions(() => {
if (!DDS.isTab) {
handleBack.current = BackHandler.addEventListener('hardwareBackPress', onHardwareBackPress);
}
});
}, []);
const onCallClear = useCallback(async (value: string) => {
if (value === 'removeHandler') {
if (handleBack.current) {
handleBack.current.remove();
}
return;
}
if (value === 'addHandler') {
if (handleBack.current) {
handleBack.current.remove();
}
handleBack.current = BackHandler.addEventListener('hardwareBackPress', onHardwareBackPress);
return;
}
if (editorState().currentlyEditing) {
await onBackPress();
}
}, []);
useEffect(() => {
if (fullscreen && DDS.isTab) {
handleBack.current = BackHandler.addEventListener('hardwareBackPress', onHardwareBackPress);
}
return () => {
if (handleBack.current) {
handleBack.current.remove();
}
};
}, [fullscreen]);
useEffect(() => {
eSubscribeEvent(eOnLoadNote, onLoadNote);
eSubscribeEvent(eClearEditor, onCallClear);
return () => {
eUnSubscribeEvent(eClearEditor, onCallClear);
eUnSubscribeEvent(eOnLoadNote, onLoadNote);
};
}, []);
const onMessage = useCallback(
event => {
event;
const data = event.nativeEvent.data;
let editorMessage = JSON.parse(data) as EditorMessage;
logger.info('editor', editorMessage.type);
if (
editorMessage.sessionId !== editor.sessionId &&
editorMessage.type !== EditorEvents.status
) {
logger.error(
'editor',
'invalid session',
editorMessage.type,
editor.sessionId,
editorMessage.sessionId
);
return;
}
switch (editorMessage.type) {
case EventTypes.logger:
logger.info('[WEBVIEW LOG]', editorMessage.value);
break;
case EventTypes.content:
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value
});
break;
case EventTypes.selection:
break;
case EventTypes.title:
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value
});
break;
case EventTypes.newtag:
if (!editor.note.current) return;
eSendEvent(eOpenTagsDialog, editor.note.current);
break;
case EventTypes.tag:
if (editorMessage.value) {
if (!editor.note.current) return;
db.notes
//@ts-ignore
?.note(editor.note.current?.id)
.untag(editorMessage.value)
.then(async () => {
useTagStore.getState().setTags();
await editor.commands.setTags(editor.note.current);
Navigation.queueRoutesForUpdate(
'ColoredNotes',
'Notes',
'TaggedNotes',
'TopicNotes',
'Tags'
);
});
}
break;
case EventTypes.filepicker:
picker.pick();
break;
case EventTypes.download:
filesystem.downloadAttachment(editorMessage.value?.hash, true);
break;
case EventTypes.pro:
if (editor.state.current?.isFocused) {
editor.state.current.isFocused = true;
}
umami.pageView('/pro-screen', '/editor');
eSendEvent(eOpenPremiumDialog);
break;
case EventTypes.monograph:
publishNote();
break;
case EventTypes.properties:
showActionsheet();
break;
case EventTypes.back:
onBackPress();
break;
default:
console.log(
'unhandled event recieved from editor: ',
editorMessage.type,
editorMessage.value
);
break;
}
eSendEvent(editorMessage.type, editorMessage);
},
[editor.sessionId]
);
return onMessage;
};

View File

@@ -58,6 +58,10 @@ export const setOnFirstSave = (
color?: string;
} | null
) => {
if (!data) {
editorState().onNoteCreated = null;
return;
}
//@ts-ignore
editorState().onNoteCreated = onNoteCreated(id, data);
};

View File

@@ -1,13 +1,49 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useEffect } from 'react';
import { FlatList, View } from 'react-native';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, FlatList, View } from 'react-native';
import Animated, { FadeIn, FadeInDown, Layout, ZoomIn } from 'react-native-reanimated';
import { Empty } from '../../components/list/empty';
import useNavigationStore from '../../stores/use-navigation-store';
import { useThemeStore } from '../../stores/use-theme-store';
import { tabBarRef } from '../../utils/global-refs';
import { useNavigationFocus } from '../../utils/hooks/use-navigation-focus';
import { SectionItem } from './section-item';
import { RouteParams, SettingSection } from './types';
const useDelayLayout = (delay: number) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
let timeout = setTimeout(() => {
setLoading(false);
}, delay);
return () => {
clearTimeout(timeout);
};
}, []);
return loading;
};
const Loading = () => {
const colors = useThemeStore(state => state.colors.accent);
return (
<Empty
placeholderData={{
paragraph: 'Personalize Notesnook the way you wish to.',
heading: 'Minimal & meaningful',
loading: 'Personalize Notesnook the way you wish to.'
}}
headerProps={{
color: 'accent'
}}
/>
);
};
const Group = ({ navigation, route }: NativeStackScreenProps<RouteParams, 'SettingsGroup'>) => {
const loading = useDelayLayout(300);
useNavigationFocus(navigation, {
onFocus: () => {
tabBarRef.current?.lock();
@@ -32,14 +68,18 @@ const Group = ({ navigation, route }: NativeStackScreenProps<RouteParams, 'Setti
<SectionItem item={item} />
);
return (
return loading ? (
<Loading />
) : (
<View>
{route.params.sections ? (
<Animated.View entering={FadeInDown}>
<FlatList
data={route.params.sections}
keyExtractor={(item, index) => item.name || index.toString()}
renderItem={renderItem}
/>
</Animated.View>
) : null}
</View>
);