mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-05-18 13:16:11 +02:00
mobile: add readonly editor
This commit is contained in:
committed by
Abdullah Atta
parent
c7142e409b
commit
43cb5d278c
@@ -31,7 +31,7 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
@@ -192,7 +192,7 @@ export default async function downloadAttachment(
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,19 +207,21 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
console.log(
|
||||
"starting decrypt base64 file...",
|
||||
options.base64,
|
||||
options.text,
|
||||
attachment.hash
|
||||
);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
@@ -232,6 +234,7 @@ export default async function downloadAttachment(
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
@@ -245,7 +248,6 @@ export default async function downloadAttachment(
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
|
||||
@@ -29,13 +29,14 @@ import { createCacheDir, exists } from "./io";
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
|
||||
console.log("Downloading", filename);
|
||||
await createCacheDir();
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
console.log("Exists already", filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,14 @@ import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
hideSheet,
|
||||
openVault,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
@@ -64,7 +62,7 @@ export const openNote = async (
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.conflicted) {
|
||||
if (!note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, note);
|
||||
return;
|
||||
}
|
||||
@@ -74,9 +72,7 @@ export const openNote = async (
|
||||
|
||||
const content = await db.content.get(note.contentId as string);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
|
||||
)
|
||||
component: <NotePreview note={item} content={content} />
|
||||
});
|
||||
} else {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
|
||||
@@ -25,7 +25,8 @@ import { SafeAreaView, Text, View } from "react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import Editor from "../../screens/editor";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
@@ -38,7 +39,6 @@ import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import DialogButtons from "../dialog/dialog-buttons";
|
||||
import DialogContainer from "../dialog/dialog-container";
|
||||
@@ -47,7 +47,6 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -316,23 +315,14 @@ const MergeConflicts = () => {
|
||||
borderBottomColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
noHeader
|
||||
noToolbar
|
||||
readonly
|
||||
editorId=":conflictPrimary"
|
||||
onLoad={async () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictPrimary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictPrimary", {
|
||||
item: {
|
||||
...note,
|
||||
content: {
|
||||
...content.current,
|
||||
isPreview: true
|
||||
}
|
||||
}
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: content.current.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -353,20 +343,14 @@ const MergeConflicts = () => {
|
||||
borderRadius: 10
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
noHeader
|
||||
noToolbar
|
||||
readonly
|
||||
editorId=":conflictSecondary"
|
||||
onLoad={async () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictSecondary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictSecondary", {
|
||||
item: {
|
||||
...note,
|
||||
content: { ...content.current.conflicted, isPreview: true }
|
||||
}
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: content.current.conflicted.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -42,7 +43,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
*/
|
||||
export default function NotePreview({ session, content, note }) {
|
||||
const { colors } = useThemeColors();
|
||||
const editorId = ":noteHistory";
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
async function restore() {
|
||||
@@ -118,22 +118,16 @@ export default function NotePreview({ session, content, note }) {
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
noHeader
|
||||
noToolbar
|
||||
readonly
|
||||
editorId={editorId}
|
||||
onLoad={async () => {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
eSendEvent(eOnLoadNote + editorId, {
|
||||
item: {
|
||||
..._note,
|
||||
content: {
|
||||
...content,
|
||||
isPreview: true
|
||||
}
|
||||
}
|
||||
});
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -101,17 +101,20 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
const renderKey = useRef(`editor-0`);
|
||||
const renderKey = useRef(`editor-0` + editorId);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
|
||||
renderKey.current === `editor-0`
|
||||
? `editor-1` + editorId
|
||||
: `editor-0` + editorId;
|
||||
|
||||
editor.state.current.ready = false;
|
||||
editor.setLoading(true);
|
||||
}, [editor]);
|
||||
}, [editor, editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [eSubscribeEvent("webview_reset", onError)];
|
||||
|
||||
287
apps/mobile/app/screens/editor/readonly-editor.tsx
Normal file
287
apps/mobile/app/screens/editor/readonly-editor.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Platform, View, ViewStyle } from "react-native";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import {
|
||||
ShouldStartLoadRequest,
|
||||
WebViewMessageEvent
|
||||
} from "react-native-webview/lib/WebViewTypes";
|
||||
import WebView from "react-native-webview";
|
||||
import { useRef } from "react";
|
||||
import { EDITOR_URI } from "./source";
|
||||
import { EditorMessage } from "./tiptap/types";
|
||||
import { EventTypes } from "./tiptap/editor-events";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { EditorEvents } from "./tiptap/utils";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
|
||||
if (request.url.includes("https")) {
|
||||
if (Platform.OS === "ios" && !request.isTopFrame) return true;
|
||||
openLinkInBrowser(request.url);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
backgroundColor: "transparent"
|
||||
};
|
||||
|
||||
export function ReadonlyEditor(props: {
|
||||
onLoad: (
|
||||
loadContent: (content: { data: string; id: string }) => void
|
||||
) => void;
|
||||
editorId: string;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const editorRef = useRef<WebView>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const noteId = useRef<string>();
|
||||
const onMessage = (event: WebViewMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
const editorMessage = JSON.parse(data) as EditorMessage<any>;
|
||||
|
||||
if (editorMessage.type === EventTypes.logger) {
|
||||
logger.info("[READONLY EDITOR LOG]", editorMessage.value);
|
||||
}
|
||||
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
const attachment = (editorMessage.value as any).attachment as Attachment;
|
||||
|
||||
console.log("Getting attachment data:", attachment.hash, attachment.type);
|
||||
downloadAttachment(attachment.hash, true, {
|
||||
base64: attachment.type === "image",
|
||||
text: attachment.type === "web-clip",
|
||||
silent: true,
|
||||
groupId: noteId.current,
|
||||
cache: true
|
||||
} as any)
|
||||
.then((data: any) => {
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
(editorMessage.value as any).resolverId
|
||||
);
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: EditorEvents.attachmentData,
|
||||
value: {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Error downloading attachment data");
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: EditorEvents.attachmentData,
|
||||
data: {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const groupId = noteId.current;
|
||||
return () => {
|
||||
if (groupId) {
|
||||
db.fs().cancel(groupId);
|
||||
}
|
||||
};
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebView
|
||||
ref={editorRef}
|
||||
key={"readonly-editor:" + props.editorId}
|
||||
nestedScrollEnabled
|
||||
injectedJavaScriptBeforeContentLoaded={`globalThis.readonlyEditor=true;`}
|
||||
injectedJavaScript="globalThis.readonlyEditor=true;"
|
||||
useSharedProcessPool={false}
|
||||
javaScriptEnabled={true}
|
||||
focusable={true}
|
||||
setSupportMultipleWindows={false}
|
||||
overScrollMode="never"
|
||||
scrollEnabled={false}
|
||||
keyboardDisplayRequiresUserAction={false}
|
||||
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||
cacheMode="LOAD_DEFAULT"
|
||||
cacheEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
bounces={false}
|
||||
setBuiltInZoomControls={false}
|
||||
setDisplayZoomControls={false}
|
||||
allowFileAccess={true}
|
||||
scalesPageToFit={true}
|
||||
hideKeyboardAccessoryView={false}
|
||||
allowsFullscreenVideo={true}
|
||||
allowFileAccessFromFileURLs={true}
|
||||
allowUniversalAccessFromFileURLs={true}
|
||||
originWhitelist={["*"]}
|
||||
source={{
|
||||
uri: EDITOR_URI
|
||||
}}
|
||||
style={style}
|
||||
autoManageStatusBarEnabled={false}
|
||||
onMessage={onMessage || undefined}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 100
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderRadius: 5,
|
||||
height: "100%",
|
||||
alignItems: "flex-start",
|
||||
paddingTop: insets.top
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 25,
|
||||
width: "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 12,
|
||||
width: "100%",
|
||||
marginTop: 10,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 12,
|
||||
width: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
height: 12,
|
||||
width: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
height: 12,
|
||||
width: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 16,
|
||||
width: "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 16,
|
||||
width: "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 16,
|
||||
width: 200,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -46,5 +46,6 @@ export const EventTypes = {
|
||||
load: "editor-events:load",
|
||||
unlock: "editor-events:unlock",
|
||||
unlockWithBiometrics: "editor-events:unlock-biometrics",
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode"
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode",
|
||||
readonlyEditorLoaded: "readonlyEditorLoaded"
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ async function createNotes(bundle) {
|
||||
|
||||
if (attached) {
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import Tiptap from "./components/editor";
|
||||
import { TabContext, useTabStore } from "./hooks/useTabStore";
|
||||
import { EmotionEditorTheme } from "./theme-factory";
|
||||
import { getTheme } from "./utils";
|
||||
import { ReadonlyEditorProvider } from "./components/readonly-editor";
|
||||
|
||||
const currentTheme = getTheme();
|
||||
if (currentTheme) {
|
||||
@@ -43,13 +44,18 @@ function App(): JSX.Element {
|
||||
<ScopedThemeProvider value="base">
|
||||
<EmotionEditorTheme>
|
||||
<GlobalStyles />
|
||||
{tabs.map((tab) => (
|
||||
<TabContext.Provider key={tab.id} value={tab}>
|
||||
<Freeze freeze={currentTab !== tab.id}>
|
||||
<Tiptap />
|
||||
</Freeze>
|
||||
</TabContext.Provider>
|
||||
))}
|
||||
|
||||
{globalThis["readonlyEditor"] ? (
|
||||
<ReadonlyEditorProvider />
|
||||
) : (
|
||||
tabs.map((tab) => (
|
||||
<TabContext.Provider key={tab.id} value={tab}>
|
||||
<Freeze freeze={currentTab !== tab.id}>
|
||||
<Tiptap />
|
||||
</Freeze>
|
||||
</TabContext.Provider>
|
||||
))
|
||||
)}
|
||||
</EmotionEditorTheme>
|
||||
</ScopedThemeProvider>
|
||||
);
|
||||
|
||||
306
packages/editor-mobile/src/components/readonly-editor.tsx
Normal file
306
packages/editor-mobile/src/components/readonly-editor.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
PortalProvider,
|
||||
TiptapOptions,
|
||||
getFontById,
|
||||
useTiptap
|
||||
} from "@notesnook/editor";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useSettings } from "../hooks/useSettings";
|
||||
import { EventTypes, Settings, isReactNative, randId } from "../utils";
|
||||
|
||||
export const ReadonlyEditorProvider = (): JSX.Element => {
|
||||
const settings = useSettings();
|
||||
const { colors } = useThemeColors("editor");
|
||||
const contentRef = useRef<HTMLElement>();
|
||||
const getContentDiv = useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
return contentRef.current;
|
||||
}
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.padding = "0px 12px";
|
||||
editorContainer.style.color = colors.primary.paragraph;
|
||||
editorContainer.style.paddingBottom = `150px`;
|
||||
editorContainer.style.fontSize = `${settings.fontSize}px`;
|
||||
editorContainer.style.fontFamily =
|
||||
getFontById(settings.fontFamily)?.font || "sans-serif";
|
||||
contentRef.current = editorContainer;
|
||||
return editorContainer;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.color = colors.primary.paragraph;
|
||||
}
|
||||
}, [colors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.fontSize = `${settings.fontSize}px`;
|
||||
contentRef.current.style.fontFamily =
|
||||
getFontById(settings.fontFamily)?.font || "sans-serif";
|
||||
}
|
||||
}, [settings.fontSize, settings.fontFamily]);
|
||||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Tiptap settings={settings} getContentDiv={getContentDiv} />
|
||||
</PortalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Tiptap = ({
|
||||
settings,
|
||||
getContentDiv
|
||||
}: {
|
||||
settings: Settings;
|
||||
getContentDiv: () => HTMLElement;
|
||||
}) => {
|
||||
const contentPlaceholderRef = useRef<HTMLDivElement>(null);
|
||||
const { colors } = useThemeColors();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorRoot = useRef<HTMLDivElement>(null);
|
||||
const content = useRef();
|
||||
const [tick, setTick] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const update = () => setTick((tick) => tick + 1);
|
||||
|
||||
const tiptapOptions = useMemo<Partial<TiptapOptions>>(() => {
|
||||
return {
|
||||
getAttachmentData(attachment) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const resolverId = randId("get_attachment_data");
|
||||
pendingResolvers[resolverId] = (data) => {
|
||||
delete pendingResolvers[resolverId];
|
||||
resolve(data);
|
||||
};
|
||||
post(EventTypes.getAttachmentData, {
|
||||
attachment,
|
||||
resolverId: resolverId
|
||||
});
|
||||
});
|
||||
},
|
||||
element: getContentDiv(),
|
||||
editable: false,
|
||||
editorProps: {
|
||||
editable: () => false
|
||||
},
|
||||
content: content.current,
|
||||
isMobile: true,
|
||||
doubleSpacedLines: settings.doubleSpacedLines,
|
||||
downloadOptions: {
|
||||
corsHost: settings.corsProxy
|
||||
},
|
||||
dateFormat: settings.dateFormat,
|
||||
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
|
||||
enableInputRules: settings.markdownShortcuts
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
getContentDiv,
|
||||
settings.doubleSpacedLines,
|
||||
settings.corsProxy,
|
||||
settings.dateFormat,
|
||||
settings.timeFormat,
|
||||
settings.markdownShortcuts,
|
||||
tick
|
||||
]);
|
||||
|
||||
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!getContentDiv().parentElement) {
|
||||
contentPlaceholderRef.current?.appendChild(getContentDiv());
|
||||
}
|
||||
}, [getContentDiv]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReactNative()) return; // Subscribe only in react native webview.
|
||||
const isSafari = navigator.vendor.match(/apple/i);
|
||||
let root: Document | Window = document;
|
||||
if (isSafari) {
|
||||
root = window;
|
||||
}
|
||||
post(EventTypes.readonlyEditorLoaded);
|
||||
|
||||
const onMessage = (event: any) => {
|
||||
if (event?.data?.[0] !== "{") return;
|
||||
const message = JSON.parse(event.data);
|
||||
const type = message.type;
|
||||
const value = message.value;
|
||||
|
||||
if (type === "native:html") {
|
||||
content.current = value;
|
||||
update();
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "native:attachment-data") {
|
||||
if (pendingResolvers[value.resolverId]) {
|
||||
logger("info", "resolved data for attachment", value.resolverId);
|
||||
pendingResolvers[value.resolverId](value.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
root.addEventListener("message", onMessage);
|
||||
|
||||
return () => {
|
||||
root.removeEventListener("message", onMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
maxWidth: "100vw"
|
||||
}}
|
||||
ref={editorRoot}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
overflowY: loading ? "hidden" : "scroll",
|
||||
height: "100%",
|
||||
display: "block",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "90%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingRight: 12,
|
||||
paddingLeft: 12,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxSizing: "border-box",
|
||||
rowGap: 10
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 25,
|
||||
width: "94%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
display: "flex",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
width: 40,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
width: 50,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
width: 100,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 16,
|
||||
width: "94%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 16,
|
||||
width: "94%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 16,
|
||||
width: 200,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div ref={contentPlaceholderRef} className="theme-scope-editor" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -103,7 +103,7 @@ export type EditorController = {
|
||||
setTitlePlaceholder: React.Dispatch<React.SetStateAction<string>>;
|
||||
countWords: (ms: number) => void;
|
||||
copyToClipboard: (text: string) => void;
|
||||
getAttachmentData: (attachment: Attachment) => Promise<string>;
|
||||
getAttachmentData: (attachment: Partial<Attachment>) => Promise<string>;
|
||||
updateTab: () => void;
|
||||
loading: boolean;
|
||||
setLoading: (value: boolean) => void;
|
||||
@@ -327,7 +327,7 @@ export function useEditorController({
|
||||
post(EventTypes.copyToClipboard, text);
|
||||
};
|
||||
|
||||
const getAttachmentData = (attachment: Attachment) => {
|
||||
const getAttachmentData = (attachment: Partial<Attachment>) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const resolverId = randId("get_attachment_data");
|
||||
pendingResolvers[resolverId] = (data) => {
|
||||
|
||||
@@ -57,6 +57,7 @@ declare global {
|
||||
var pendingResolvers: {
|
||||
[key: string]: (value: any) => void;
|
||||
};
|
||||
var readonlyEditor: boolean;
|
||||
var statusBars: Record<
|
||||
number,
|
||||
| React.MutableRefObject<{
|
||||
@@ -189,7 +190,8 @@ export const EventTypes = {
|
||||
load: "editor-events:load",
|
||||
unlock: "editor-events:unlock",
|
||||
unlockWithBiometrics: "editor-events:unlock-biometrics",
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode"
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode",
|
||||
readonlyEditorLoaded: "readonlyEditorLoaded"
|
||||
} as const;
|
||||
|
||||
export function randId(prefix: string) {
|
||||
|
||||
Reference in New Issue
Block a user