diff --git a/apps/mobile/app/common/filesystem/download-attachment.js b/apps/mobile/app/common/filesystem/download-attachment.js index 3fa79c97a..1ca2a5547 100644 --- a/apps/mobile/app/common/filesystem/download-attachment.js +++ b/apps/mobile/app/common/filesystem/download-attachment.js @@ -215,13 +215,13 @@ export default async function downloadAttachment( await db .fs() .downloadFile(options.groupId || attachment.hash, attachment.hash); - if (!(await exists(attachment.metadata.hash))) { + if (!(await exists(attachment.hash))) { return; } if (options.base64 || options.text) { return await db.attachments.read( - attachment.metadata.hash, + attachment.hash, options.base64 ? "base64" : "text" ); } diff --git a/apps/mobile/app/components/dialog/functions.ts b/apps/mobile/app/components/dialog/functions.ts index e1c05c06f..e7f601cbe 100644 --- a/apps/mobile/app/components/dialog/functions.ts +++ b/apps/mobile/app/components/dialog/functions.ts @@ -41,7 +41,9 @@ type DialogInfo = { input: boolean; inputPlaceholder: string; defaultValue: string; - context: "global" | "local"; + // eslint-disable-next-line @typescript-eslint/ban-types + context: "global" | "local" | (string & {}); + secureTextEntry?: boolean; }; export function presentDialog(data: Partial): void { diff --git a/apps/mobile/app/components/sheets/export-notes/index.tsx b/apps/mobile/app/components/sheets/export-notes/index.tsx index 2d074fc8e..29b429e3d 100644 --- a/apps/mobile/app/components/sheets/export-notes/index.tsx +++ b/apps/mobile/app/components/sheets/export-notes/index.tsx @@ -47,6 +47,7 @@ import { PressableButton } from "../../ui/pressable"; import Seperator from "../../ui/seperator"; import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; +import { Dialog } from "../../dialog"; const ExportNotesSheet = ({ ids, @@ -67,10 +68,12 @@ const ExportNotesSheet = ({ } | undefined >(); - const [status, setStatus] = useState(null); + const [status, setStatus] = useState(); const premium = useUserStore((state) => state.premium); - const save = async (type: "pdf" | "txt" | "md" | "html") => { + const save = async ( + type: "pdf" | "txt" | "md" | "html" | "md-frontmatter" + ) => { if (exporting) return; if (!PremiumService.get() && type !== "txt") return; setExporting(true); diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 158438519..f2bfd1bc3 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -329,7 +329,9 @@ export const useAppEvents = () => { if (Platform.OS === "android") { try { await RNIap.flushFailedPurchasesCachedAsPendingAndroid(); - } catch (e) {} + } catch (e) { + e; + } } refValues.current.subsriptionSuccessListener = RNIap.purchaseUpdatedListener(onSuccessfulSubscription); @@ -648,11 +650,7 @@ export const useAppEvents = () => { if (!db.isInitialized) { RNBootSplash.hide({ fade: true }); DatabaseLogger.info("Initializing database"); - try { - await db.init(); - } catch (e) { - console.log(e); - } + await db.init(); } if (IsDatabaseMigrationRequired()) return; setImmediate(() => { diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts b/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts index 29817da01..4c14dea71 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts @@ -19,9 +19,10 @@ along with this program. If not, see . /* eslint-disable no-case-declarations */ /* eslint-disable @typescript-eslint/no-var-requires */ -import Clipboard from "@react-native-clipboard/clipboard"; +import { ItemReference } from "@notesnook/core/dist/types"; import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index"; import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions"; +import Clipboard from "@react-native-clipboard/clipboard"; import { useCallback, useEffect, useRef } from "react"; import { BackHandler, @@ -33,6 +34,7 @@ import { } from "react-native"; import { WebViewMessageEvent } from "react-native-webview"; import { db } from "../../../common/database"; +import downloadAttachment from "../../../common/filesystem/download-attachment"; import ManageTagsSheet from "../../../components/sheets/manage-tags"; import { RelationsList } from "../../../components/sheets/relations-list"; import ReminderSheet from "../../../components/sheets/reminder"; @@ -44,7 +46,9 @@ import { eUnSubscribeEvent } from "../../../services/event-manager"; import Navigation from "../../../services/navigation"; +import SettingsService from "../../../services/settings"; import { useEditorStore } from "../../../stores/use-editor-store"; +import { useRelationStore } from "../../../stores/use-relation-store"; import { useSettingStore } from "../../../stores/use-setting-store"; import { useTagStore } from "../../../stores/use-tag-store"; import { useUserStore } from "../../../stores/use-user-store"; @@ -63,11 +67,6 @@ import { useDragState } from "../../settings/editor/state"; import { EventTypes } from "./editor-events"; import { EditorMessage, EditorProps, useEditorType } from "./types"; import { EditorEvents, editorState } from "./utils"; -import { useNoteStore } from "../../../stores/use-notes-store"; -import SettingsService from "../../../services/settings"; -import downloadAttachment from "../../../common/filesystem/download-attachment"; -import { ItemReference } from "@notesnook/core/dist/types"; -import { useRelationStore } from "../../../stores/use-relation-store"; const publishNote = async (editor: useEditorType) => { const user = useUserStore.getState().user; diff --git a/apps/mobile/app/screens/notebooks/index.tsx b/apps/mobile/app/screens/notebooks/index.tsx index b7059a1d1..40c0236e2 100644 --- a/apps/mobile/app/screens/notebooks/index.tsx +++ b/apps/mobile/app/screens/notebooks/index.tsx @@ -89,6 +89,7 @@ export const Notebooks = ({ data={notebooks} dataType="notebook" renderedInRoute="Notebooks" + loading={loading} placeholder={{ title: "Your notebooks", paragraph: "You have not added any notebooks yet.", diff --git a/apps/mobile/app/services/event-manager.ts b/apps/mobile/app/services/event-manager.ts index 94152fa3c..90603b946 100644 --- a/apps/mobile/app/services/event-manager.ts +++ b/apps/mobile/app/services/event-manager.ts @@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import EventManager from "@notesnook/core/dist/utils/event-manager"; +import EventManager, { + EventHandler +} from "@notesnook/core/dist/utils/event-manager"; import Clipboard from "@react-native-clipboard/clipboard"; import { RefObject } from "react"; import { ActionSheetRef } from "react-native-actions-sheet"; @@ -60,14 +62,14 @@ const eventManager = new EventManager(); export const eSubscribeEvent = ( eventName: string, - action?: (data: T) => void + action: EventHandler ) => { return eventManager.subscribe(eventName, action); }; export const eUnSubscribeEvent = ( eventName: string, - action?: (data: T) => void + action: EventHandler ) => { eventManager.unsubscribe(eventName, action); }; @@ -116,6 +118,7 @@ export type PresentSheetOptions = { learnMorePress: () => void; enableGesturesInScrollView?: boolean; noBottomPadding?: boolean; + keyboardHandlerDisabled?: boolean; }; export function presentSheet(data: Partial) { diff --git a/apps/mobile/app/services/exporter.js b/apps/mobile/app/services/exporter.ts similarity index 65% rename from apps/mobile/app/services/exporter.js rename to apps/mobile/app/services/exporter.ts index 9fa211efd..2c1c59cb8 100644 --- a/apps/mobile/app/services/exporter.js +++ b/apps/mobile/app/services/exporter.ts @@ -27,62 +27,57 @@ import { DatabaseLogger, db } from "../common/database/index"; import Storage from "../common/database/storage"; import { sanitizeFilename } from "@notesnook/common"; +import { Note } from "@notesnook/core"; +import { NoteContent } from "@notesnook/core/dist/collections/session-content"; import { presentDialog } from "../components/dialog/functions"; import { useSettingStore } from "../stores/use-setting-store"; import BiometicService from "./biometrics"; -import { ToastEvent } from "./event-manager"; +import { ToastManager } from "./event-manager"; const MIMETypes = { txt: "text/plain", pdf: "application/pdf", md: "text/markdown", + "md-frontmatter": "text/markdown", html: "text/html" }; -const FolderNames = { +const FolderNames: { [name: string]: string } = { txt: "Text", pdf: "PDF", md: "Markdown", html: "Html" }; -async function releasePermissions(path) { +async function releasePermissions(path: string) { if (Platform.OS === "ios") return; const uris = await ScopedStorage.getPersistedUriPermissions(); - for (let uri of uris) { + for (const uri of uris) { if (path.startsWith(uri)) { await ScopedStorage.releasePersistableUriPermission(uri); } } } -/** - * - * @param {"Text" | "PDF" | "Markdown" | "Html" } type - * @returns - */ -async function getPath(type) { +async function getPath(type: string) { let path = Platform.OS === "ios" && (await Storage.checkAndCreateDir(`/exported/${type}/`)); if (Platform.OS === "android") { - let file = await ScopedStorage.openDocumentTree(true); + const file = await ScopedStorage.openDocumentTree(true); if (!file) return; path = file.uri; } return path; } -/** - * - * @param {string} path - * @param {string} data - * @param {string} title - * @param {"txt" | "pdf" | "md" | "html"} extension - * @returns - */ -async function save(path, data, fileName, extension) { +async function save( + path: string, + data: string, + fileName: string, + extension: "txt" | "pdf" | "md" | "html" | "md-frontmatter" +) { let uri; if (Platform.OS === "android") { uri = await ScopedStorage.writeFile( @@ -101,21 +96,25 @@ async function save(path, data, fileName, extension) { return uri || path; } -async function makeHtml(note) { +async function makeHtml(note: Note, content?: NoteContent) { let html = await db.notes.export(note.id, { - format: "html" + format: "html", + contentItem: content }); + if (!html) return ""; + html = decode(html, { level: EntityLevel.HTML }); return html; } -/** - * - * @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type - */ -async function exportAs(type, note, bulk, content) { +async function exportAs( + type: string, + note: Note, + bulk?: boolean, + content?: NoteContent +) { let data; switch (type) { case "html": @@ -125,22 +124,24 @@ async function exportAs(type, note, bulk, content) { break; case "md": data = await db.notes.export(note.id, { - format: "md" + format: "md", + contentItem: content }); break; case "md-frontmatter": - data = await db.notes - .note(note.id) - .export("md-frontmatter", content?.data); + data = await db.notes.export(note.id, { + format: "md-frontmatter", + contentItem: content + }); break; case "pdf": { - let html = await makeHtml(note, content); - let fileName = sanitizeFilename(note.title + Date.now(), { + const html = await makeHtml(note, content); + const fileName = sanitizeFilename(note.title + Date.now(), { replacement: "_" }); - let options = { + const options = { html: html, fileName: Platform.OS === "ios" ? "/exported/PDF/" + fileName : fileName, @@ -149,11 +150,12 @@ async function exportAs(type, note, bulk, content) { bgColor: "#FFFFFF", padding: 30, base64: bulk || Platform.OS === "android" - }; + } as { [name: string]: any }; + if (Platform.OS === "ios") { options.directory = "Documents"; } - let res = await RNHTMLtoPDF.convert(options); + const res = await RNHTMLtoPDF.convert(options); data = !bulk && Platform.OS === "ios" ? res.filePath : res.base64; if (bulk && res.filePath) { RNFetchBlob.fs.unlink(res.filePath); @@ -162,7 +164,10 @@ async function exportAs(type, note, bulk, content) { break; case "txt": { - data = await db.notes?.note(note.id).export("txt", content); + data = await db.notes.export(note.id, { + format: "txt", + contentItem: content + }); } break; } @@ -171,10 +176,10 @@ async function exportAs(type, note, bulk, content) { } async function unlockVault() { - let biometry = await BiometicService.isBiometryAvailable(); - let fingerprint = await BiometicService.hasInternetCredentials("nn_vault"); + const biometry = await BiometicService.isBiometryAvailable(); + const fingerprint = await BiometicService.hasInternetCredentials(); if (biometry && fingerprint) { - let credentials = await BiometicService.getCredentials( + const credentials = await BiometicService.getCredentials( "Unlock vault", "Unlock vault to export locked notes" ); @@ -196,7 +201,7 @@ async function unlockVault() { positivePress: async (value) => { const unlocked = await db.vault.unlock(value); if (!unlocked) { - ToastEvent.show({ + ToastManager.show({ heading: "Invalid password", message: "Please enter a valid password", type: "error", @@ -217,11 +222,10 @@ async function unlockVault() { }); } -/** - * - * @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type - */ -async function exportNote(id, type) { +async function exportNote( + id: string, + type: "txt" | "pdf" | "md" | "html" | "md-frontmatter" +) { const note = await db.notes.note(id); if (!note) return; @@ -229,21 +233,21 @@ async function exportNote(id, type) { if (note.locked) { try { - let unlocked = await unlockVault(); + const unlocked = await unlockVault(); if (!unlocked) return null; const unlockedNote = await db.vault.open(note.id); - content = unlockedNote.content; + content = unlockedNote?.content; } catch (e) { - DatabaseLogger.error(e); + DatabaseLogger.error(e as Error); } } let path = await getPath(FolderNames[type]); if (!path) return; - let result = await exportAs(type, note, false, content); + const result = await exportAs(type, note, false, content); if (!result) return null; - let fileName = sanitizeFilename(note.title + Date.now(), { + const fileName = sanitizeFilename(note.title + Date.now(), { replacement: "_" }); @@ -261,32 +265,36 @@ async function exportNote(id, type) { }; } -function copyFileAsync(source, dest) { +function copyFileAsync(source: string, dest: string) { return new Promise((resolve) => { ScopedStorage.copyFile(source, dest, () => { - resolve(); + resolve(true); }); }); } -function getUniqueFileName(fileName, results) { +function getUniqueFileName( + fileName: string, + notebookPath: string, + results: { [name: string]: boolean } +) { const chunks = fileName.split("."); const ext = chunks.pop(); const name = chunks.join("."); let resolvedName = fileName; let count = 0; - while (results[resolvedName]) { + while (results[`${notebookPath}${resolvedName}`]) { resolvedName = `${name}${++count}.${ext}`; } return resolvedName; } -/** - * - * @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type - */ -async function bulkExport(ids, type, callback) { +async function bulkExport( + ids: string[], + type: "txt" | "pdf" | "md" | "html" | "md-frontmatter", + callback: (progress?: string) => void +) { let path = await getPath(FolderNames[type]); if (!path) return; @@ -295,14 +303,14 @@ async function bulkExport(ids, type, callback) { await RNFetchBlob.fs.mkdir(exportCacheFolder).catch((e) => console.log(e)); - const mkdir = async (dir) => { + const mkdir = async (dir: string) => { const folder = `${exportCacheFolder}/${dir}`; if (!(await RNFetchBlob.fs.exists(folder))) { await RNFetchBlob.fs.mkdir(folder); } }; - const writeFile = async (path, result) => { + const writeFile = async (path: string, result: string) => { const cacheFilePath = exportCacheFolder + path; await RNFetchBlob.fs.writeFile( cacheFilePath, @@ -311,94 +319,69 @@ async function bulkExport(ids, type, callback) { ); }; - const results = {}; - for (var i = 0; i < ids.length; i++) { + const results: { [name: string]: boolean } = {}; + for (let i = 0; i < ids.length; i++) { try { - let note = await db.notes.note(ids[i]); + const note = await db.notes.note(ids[i]); if (!note) continue; let content; if (note.locked) { try { - let unlocked = !db.vault.unlocked ? await unlockVault() : true; + const unlocked = !db.vault.unlocked ? await unlockVault() : true; if (!unlocked) { continue; } const unlockedNote = await db.vault.open(note.id); - content = unlockedNote.content; + content = unlockedNote?.content; } catch (e) { - DatabaseLogger.error(e); + DatabaseLogger.error(e as Error); continue; } } - let result = await exportAs(type, note, true, content); - let fileName = sanitizeFilename(note.title, { + const result = await exportAs(type, note, true, content); + const fileName = sanitizeFilename(note.title, { replacement: "_" }); if (result) { - const notebooks = [ - ...(db.relations - ?.to({ id: note.id, type: "note" }, "notebook") - .map((notebook) => ({ - title: notebook.title - })) || []), - ...(note.notebooks || []).map((ref) => { - const notebook = db.notebooks?.notebook(ref.id); - const topics = notebook?.topics.all || []; - - return { - title: notebook?.title, - topics: ref.topics - .map((topicId) => topics.find((topic) => topic.id === topicId)) - .filter(Boolean) - }; - }) - ]; + const notebooks = await db.relations + ?.to({ id: note.id, type: "note" }, "notebook") + .resolve(); for (const notebook of notebooks) { - results[notebook.title] = results[notebook.title] || {}; - await mkdir(notebook.title); + const notebookPath = (await db.notebooks.breadcrumbs(notebook.id)) + .map((notebook) => { + return notebook.title + "/"; + }) + .join(""); - if (notebook.topics && notebook.topics.length) { - for (const topic of notebook.topics) { - results[notebook.title][topic.title] = - results[notebook.title][topic.title] || {}; + await mkdir(notebookPath); - await mkdir(`${notebook.title}/${topic.title}`); - const exportedNoteName = getUniqueFileName( - fileName + `.${type}`, - results[notebook.title][topic.title] - ); - results[notebook.title][topic.title][exportedNoteName] = true; + console.log("Dir created", notebookPath); - writeFile( - `/${notebook.title}/${topic.title}/${exportedNoteName}`, - result - ); - } - } else { - const exportedNoteName = getUniqueFileName( - fileName + `.${type}`, - results[notebook.title] - ); - results[notebook.title][exportedNoteName] = true; - writeFile(`/${notebook.title}/${exportedNoteName}`, result); - } + const exportedNoteName = getUniqueFileName( + fileName + `.${type}`, + notebookPath, + results + ); + results[`${notebookPath}${exportedNoteName}`] = true; + await writeFile(`/${notebookPath}${exportedNoteName}`, result); } if (!notebooks.length) { const exportedNoteName = getUniqueFileName( fileName + `.${type}`, + "", results ); results[exportedNoteName] = true; - writeFile(`/${exportedNoteName}`, result); + await writeFile(`/${exportedNoteName}`, result); } } callback(`${i + 1}/${ids.length}`); } catch (e) { - DatabaseLogger.error(e); + DatabaseLogger.error(e as Error); } } const fileName = `nn-export-${ids.length}-${type}-${Date.now()}.zip`; @@ -424,7 +407,7 @@ async function bulkExport(ids, type, callback) { } RNFetchBlob.fs.unlink(exportCacheFolder); } catch (e) { - DatabaseLogger.error(e); + DatabaseLogger.error(e as Error); } return { diff --git a/packages/core/src/api/vault.ts b/packages/core/src/api/vault.ts index 3530e8e2a..3f33beead 100644 --- a/packages/core/src/api/vault.ts +++ b/packages/core/src/api/vault.ts @@ -183,13 +183,16 @@ export default class Vault { /** * Temporarily unlock (open) a note */ - async open(noteId: string, password: string) { + async open(noteId: string, password?: string) { const note = await this.db.notes.note(noteId); if (!note) return; const unlockedNote = await this.unlockNote(note, password, false); - this.password = password; - if (!(await this.exists())) await this.create(password); + if (password) { + this.password = password; + if (!(await this.exists())) await this.create(password); + } + return unlockedNote; } @@ -339,7 +342,7 @@ export default class Vault { }); } - private async unlockNote(note: Note, password: string, perm = false) { + private async unlockNote(note: Note, password?: string, perm = false) { if (!note.contentId) return; const encryptedContent = await this.db.content.get(note.contentId);