diff --git a/apps/mobile/app/share/editor.js b/apps/mobile/app/share/editor.tsx similarity index 80% rename from apps/mobile/app/share/editor.js rename to apps/mobile/app/share/editor.tsx index 047a9686f..1c7bde2e4 100644 --- a/apps/mobile/app/share/editor.js +++ b/apps/mobile/app/share/editor.tsx @@ -16,16 +16,22 @@ 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 . */ +import { Note, NoteContent } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { + RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Linking, Platform, TextInput, View } from "react-native"; +import { Linking, Platform, TextInput, View, ViewStyle } from "react-native"; import { WebView } from "react-native-webview"; +import { + ShouldStartLoadRequest, + WebViewMessageEvent +} from "react-native-webview/lib/WebViewTypes"; import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager"; import { eOnLoadNote } from "../utils/events"; import { defaultBorderRadius } from "../utils/size"; @@ -42,7 +48,11 @@ export const EDITOR_URI = __DEV__ ? EditorMobileSourceUrl : EditorMobileSourceUrl; -export async function post(ref, type, value = null) { +export async function post( + ref: RefObject, + type: string, + value: any = null +) { const message = { type, value @@ -51,17 +61,25 @@ export async function post(ref, type, value = null) { } const useEditor = () => { - const ref = useRef(); + const ref = useRef(null); const { colors } = useThemeColors("editor"); - const currentNote = useRef(); + const currentNote = useRef< + Note & { + content: NoteContent; + } + >(undefined); const postMessage = useCallback( - async (type, data) => post(ref, type, data), + async (type: string, data: any) => post(ref, type, data), [] ); const loadNote = useCallback( - (note) => { + ( + note: Note & { + content: NoteContent; + } + ) => { postMessage("html", note.content.data); currentNote.current = note; }, @@ -104,8 +122,11 @@ const useEditor = () => { return { ref, onLoad, currentNote }; }; -const useEditorEvents = (editor, onChange) => { - const onMessage = (event) => { +const useEditorEvents = ( + editor: ReturnType, + onChange: (data: string) => void +) => { + const onMessage = (event: WebViewMessageEvent) => { const data = event.nativeEvent.data; const editorMessage = JSON.parse(data); @@ -118,7 +139,7 @@ const useEditorEvents = (editor, onChange) => { return onMessage; }; -const onShouldStartLoadWithRequest = (request) => { +const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => { if (request.url.includes("https")) { if (Platform.OS === "ios" && !request.isTopFrame) return true; Linking.openURL(request.url); @@ -128,7 +149,7 @@ const onShouldStartLoadWithRequest = (request) => { } }; -const style = { +const style: ViewStyle = { height: "100%", maxHeight: "100%", width: "100%", @@ -136,32 +157,46 @@ const style = { backgroundColor: "transparent" }; -export const Editor = ({ onChange, onLoad, editorRef }) => { +export type EditorRef = { + focus: () => void; +}; + +export const Editor = ({ + onChange, + onLoad, + editorRef +}: { + onChange: (data: string) => void; + onLoad: () => void; + editorRef: RefObject; +}) => { const { colors } = useThemeColors(); const editor = useEditor(); - const inputRef = useRef(); + const inputRef = useRef(null); const onMessage = useEditorEvents(editor, onChange); const [loading, setLoading] = useState(true); useLayoutEffect(() => { onLoad?.(); }, [onLoad]); - if (editorRef) { - editorRef.current = { - focus: () => { - setTimeout(() => { - inputRef.current?.focus(); - editor.ref.current?.injectJavaScript(`(() => { + useEffect(() => { + if (editorRef) { + editorRef.current = { + focus: () => { + setTimeout(() => { + inputRef.current?.focus(); + editor.ref.current?.injectJavaScript(`(() => { const editor = document.getElementById('editor'); if (editor) { editor.focus(); } })();`); - editor.ref?.current?.requestFocus(); - }); - } - }; - } + editor.ref?.current?.requestFocus(); + }); + } + }; + } + }, []); return ( Promise; +}>(); + export const HtmlLoadingWebViewAgent = React.memo( () => { - const [source, setSource] = useState(null); - const [clipper, setClipper] = useState(null); - const loadHandler = useRef(); - const htmlHandler = useRef(); - const webview = useRef(); - const premium = useRef(false); + const [source, setSource] = useState(null); + const [clipper, setClipper] = useState(null); + const loadHandler = useRef<((result?: boolean | null) => void) | null>( + null + ); + const htmlHandler = useRef<((html: string | null) => void) | null>(null); + const webview = useRef(null); const corsProxy = Config.corsProxy; useImperativeHandle( @@ -71,12 +73,6 @@ export const HtmlLoadingWebViewAgent = React.memo( useEffect(() => { (async () => { - const user = await db.user.getUser(); - const subscriptionStatus = - user?.subscription?.type || SUBSCRIPTION_STATUS.BASIC; - premium.current = - user && subscriptionStatus !== SUBSCRIPTION_STATUS.BASIC; - const clipperPath = Platform.OS === "ios" ? RNFetchBlob.fs.dirs.MainBundleDir + @@ -107,7 +103,7 @@ export const HtmlLoadingWebViewAgent = React.memo( }} useSharedProcessPool={false} pointerEvents="none" - onMessage={(event) => { + onMessage={(event: any) => { try { const data = JSON.parse(event.nativeEvent.data); if (data && data.type === "html") { @@ -123,7 +119,7 @@ export const HtmlLoadingWebViewAgent = React.memo( console.log("Error handling webview message", e); } }} - injectedJavaScriptBeforeContentLoaded={script(clipper, premium.current)} + injectedJavaScriptBeforeContentLoaded={script(clipper!, corsProxy)} onError={() => { console.log("Error loading page"); loadHandler.current?.(); @@ -137,7 +133,7 @@ export const HtmlLoadingWebViewAgent = React.memo( () => true ); -const script = (clipper, pro) => ` +const script = (clipper: string, corsProxy?: string): string => ` ${clipper} function postMessage(type, value) { @@ -158,10 +154,10 @@ function postMessage(type, value) { postMessage("error", globalThis.Clipper.clipPage); } else { globalThis.Clipper.clipPage(document,false, { - images: ${pro}, + images: true, inlineImages: false, styles: false, - corsProxy: undefined + corsProxy: ${corsProxy ? `"${corsProxy}"` : `undefined`} }).then(result => { postMessage("html", result); }).catch(e => { diff --git a/apps/mobile/app/share/index.js b/apps/mobile/app/share/index.tsx similarity index 80% rename from apps/mobile/app/share/index.js rename to apps/mobile/app/share/index.tsx index f2c9a12bc..f31ac716f 100644 --- a/apps/mobile/app/share/index.js +++ b/apps/mobile/app/share/index.tsx @@ -18,19 +18,18 @@ along with this program. If not, see . */ import { ScopedThemeProvider } from "@notesnook/theme"; -import React, { Fragment, useEffect, useState } from "react"; -import { Modal, Platform } from "react-native"; +import React, { useEffect, useState } from "react"; +import { Modal, ModalProps, Platform } from "react-native"; import ShareView from "./share"; import "./store"; -const Wrapper = Platform.OS === "android" ? Modal : Fragment; const outerProps = Platform.OS === "android" - ? { + ? ({ animationType: "fade", transparent: true, visible: true - } + } as ModalProps) : {}; const NotesnookShare = ({ quicknote = false }) => { @@ -42,10 +41,12 @@ const NotesnookShare = ({ quicknote = false }) => { }, []); return ( - {!render ? null : ( - - - + {!render ? null : Platform.OS === "android" ? ( + + + + ) : ( + )} ); diff --git a/apps/mobile/app/share/share.js b/apps/mobile/app/share/share.tsx similarity index 91% rename from apps/mobile/app/share/share.js rename to apps/mobile/app/share/share.tsx index bf8ecd741..fa632a313 100644 --- a/apps/mobile/app/share/share.js +++ b/apps/mobile/app/share/share.tsx @@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import ShareExtension from "@ammarahmed/react-native-share-extension"; +import ShareExtension, { + ShareItem +} from "@ammarahmed/react-native-share-extension"; import { getPreviewData } from "@flyerhq/react-native-link-preview"; import { formatBytes, @@ -33,6 +35,7 @@ import { Dimensions, Image, Keyboard, + KeyboardEvent, Platform, SafeAreaView, ScrollView, @@ -53,21 +56,21 @@ import Heading from "../components/ui/typography/heading"; import Paragraph from "../components/ui/typography/paragraph"; import { useDBItem } from "../hooks/use-db-item"; import { eSendEvent } from "../services/event-manager"; -import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../utils/constants"; import { eOnLoadNote } from "../utils/events"; import { NoteBundle } from "../utils/note-bundle"; import { defaultBorderRadius, AppFontSize } from "../utils/size"; import { AddNotebooks } from "./add-notebooks"; import { AddTags } from "./add-tags"; -import { Editor } from "./editor"; +import { Editor, EditorRef } from "./editor"; import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview"; import { Search } from "./search"; import { initDatabase, useShareStore } from "./store"; +import { isTablet } from "react-native-device-info"; -const getLinkPreview = (url) => { +const getLinkPreview = (url: string) => { return getPreviewData(url, 5000); }; -async function sanitizeHtml(site) { +async function sanitizeHtml(site: string) { try { let html = await fetchHandle.current?.processUrl(site); return html; @@ -76,11 +79,11 @@ async function sanitizeHtml(site) { } } -function makeHtmlFromUrl(url) { +function makeHtmlFromUrl(url: string) { return `${url}`; } -function makeHtmlFromPlainText(text) { +function makeHtmlFromPlainText(text: string) { if (!text) return ""; return `

${text @@ -88,12 +91,22 @@ function makeHtmlFromPlainText(text) { .replace(/(?:\r\n|\r|\n)/g, "

")}

`; } -let defaultNote = { - title: null, - id: null, +type DefaultNote = { + title?: string; + id?: string; + sessionId?: number; + content: { + type: "tiptap"; + data?: string; + }; +}; + +let defaultNote: DefaultNote = { + title: "", + id: undefined, content: { type: "tiptap", - data: null + data: "" } }; @@ -115,32 +128,39 @@ const modes = { } }; +declare global { + var IS_SHARE_EXTENSION: boolean; + var IS_MAIN_APP_RUNNING: boolean; +} + const ShareView = () => { const { colors } = useThemeColors(); const appendNoteId = useShareStore((state) => state.appendNote); const [note, setNote] = useState({ ...defaultNote }); - const noteContent = useRef(""); - const noteTitle = useRef(""); + const noteContent = useRef(undefined); + const noteTitle = useRef(undefined); const [loading, setLoading] = useState(false); const [loadingExtension, setLoadingExtension] = useState(true); const fullQualityImages = useIsFeatureAvailable("fullQualityImages"); - const [rawData, setRawData] = useState({ - type: null, - value: null - }); - const inputRef = useRef(null); + const [rawData, setRawData] = useState<{ + type?: string; + value?: string; + }>({}); + const inputRef = useRef(null); const [mode, setMode] = useState(1); const keyboardHeight = useRef(0); const { width, height } = useWindowDimensions(); const [loadingPage, setLoadingPage] = useState(false); - const editorRef = useRef(); - const [searchMode, setSearchMode] = useState(null); - const [rawFiles, setRawFiles] = useState([]); + const editorRef = useRef(null); + const [searchMode, setSearchMode] = useState< + "appendNote" | "selectTags" | "selectNotebooks" | null + >(null); + const [rawFiles, setRawFiles] = useState([]); const [kh, setKh] = useState(0); const [compress, setCompress] = useState(true); globalThis["IS_SHARE_EXTENSION"] = true; - const onKeyboardDidShow = (event) => { + const onKeyboardDidShow = (event: KeyboardEvent) => { let height = Dimensions.get("screen").height - event.endCoordinates.screenY; keyboardHeight.current = height; setKh(height); @@ -172,7 +192,7 @@ const ShareView = () => { } }, [fullQualityImages]); - const showLinkPreview = async (note, link) => { + const showLinkPreview = async (note: DefaultNote, link: string) => { let _note = note; _note.content.data = makeHtmlFromUrl(link); try { @@ -184,20 +204,33 @@ const ShareView = () => { return note; }; + const onLoad = useCallback(() => { + console.log(noteContent.current, "current..."); + eSendEvent(eOnLoadNote + "shareEditor", { + id: null, + content: { + type: "tiptap", + data: noteContent.current + }, + forced: true + }); + }, []); + const loadData = useCallback( - async (isEditor) => { + async (isEditor: boolean) => { try { if (noteContent.current) { onLoad(); return; } - defaultNote.content.data = null; + defaultNote.content.data = undefined; setNote({ ...defaultNote }); const data = await ShareExtension.data(); if (!data || data.length === 0) { setRawData({ - value: "" + value: "", + type: "text" }); if (isEditor) { setTimeout(() => { @@ -257,7 +290,6 @@ const ShareView = () => { } } onLoad(); - setNote({ ...note }); } catch (e) { console.error(e); @@ -266,24 +298,12 @@ const ShareView = () => { [onLoad] ); - const onLoad = useCallback(() => { - console.log(noteContent.current, "current..."); - eSendEvent(eOnLoadNote + "shareEditor", { - id: null, - content: { - type: "tiptap", - data: noteContent.current - }, - forced: true - }); - }, []); - useEffect(() => { (async () => { try { await initDatabase(); setLoadingExtension(false); - loadData(); + loadData(false); useShareStore.getState().restore(); } catch (e) { DatabaseLogger.error(e); @@ -306,22 +326,23 @@ const ShareView = () => { let noteData; if (appendNoteId) { - if (!(await db.notes.exists(appendNoteId))) { + const note = await db.notes.note(appendNoteId); + if (!note) { useShareStore.getState().setAppendNote(null); Alert.alert("The note you are trying to append to has been deleted."); setLoading(false); return; } - const note = await db.notes.note(appendNoteId); - let rawContent = await db.content.get(note.contentId); - + let rawContent = note.contentId + ? await db.content.get(note.contentId) + : null; noteData = { content: { data: (rawContent?.data || "") + "
" + noteContent.current, type: "tiptap" }, - id: note.id, + id: note?.id, sessionId: Date.now() }; } else { @@ -354,28 +375,30 @@ const ShareView = () => { setLoading(false); }; - const changeMode = async (m) => { - setMode(m); + const changeMode = async (value: number) => { + setMode(value); setLoading(true); try { - if (m === 2) { + if (value === 2) { setLoadingPage(true); setTimeout(async () => { - let html = await sanitizeHtml(rawData.value); - noteContent.current = html; + let html = await sanitizeHtml(rawData?.value || ""); + noteContent.current = html || ""; setLoadingPage(false); onLoad(); setNote((note) => { - note.content.data = html; + note.content.data = html || ""; return { ...note }; }); }, 300); } else { setLoadingPage(false); - let html = isURL(rawData.value) - ? makeHtmlFromUrl(rawData.value) - : makeHtmlFromPlainText(rawData.value); + let html = !rawData.value + ? "" + : isURL(rawData?.value) + ? makeHtmlFromUrl(rawData?.value) + : makeHtmlFromPlainText(rawData?.value); setNote((note) => { note.content.data = html; noteContent.current = html; @@ -395,7 +418,7 @@ const ShareView = () => { loadData(true); }, [loadData]); - const onRemoveFile = (item) => { + const onRemoveFile = (item: ShareItem) => { const index = rawFiles.findIndex((file) => file.name === item.name); if (index > -1) { setRawFiles((state) => { @@ -529,7 +552,7 @@ const ShareView = () => { defaultValue={noteTitle.current} blurOnSubmit={false} onSubmitEditing={() => { - editorRef.current.focus(); + editorRef.current?.focus(); }} /> )} @@ -584,9 +607,6 @@ const ShareView = () => { onRemoveFile(item)} style={{ borderRadius: defaultBorderRadius, @@ -599,7 +619,6 @@ const ShareView = () => { paddingHorizontal: 8, marginRight: 6 }} - resizeMode="cover" > { @@ -848,7 +867,13 @@ const ShareView = () => { ); }; -const AppendNote = ({ id, onLoad }) => { +const AppendNote = ({ + id, + onLoad +}: { + id: string; + onLoad: (title: string) => void; +}) => { const { colors } = useThemeColors(); const [item] = useDBItem(id, "note"); diff --git a/apps/mobile/app/share/store.js b/apps/mobile/app/share/store.ts similarity index 70% rename from apps/mobile/app/share/store.js rename to apps/mobile/app/share/store.ts index ed6306206..924b026f3 100644 --- a/apps/mobile/app/share/store.js +++ b/apps/mobile/app/share/store.ts @@ -17,11 +17,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { ThemeDark, ThemeLight, useThemeEngineStore } from "@notesnook/theme"; +import { + ThemeDark, + ThemeDefinition, + ThemeLight, + useThemeEngineStore +} from "@notesnook/theme"; import { Appearance } from "react-native"; import { create } from "zustand"; import { db, setupDatabase } from "../common/database"; import { MMKV } from "../common/database/mmkv"; +import { SettingStore } from "../stores/use-setting-store"; export async function initDatabase() { if (!db.isInitialized) { @@ -37,9 +43,10 @@ const StorageKeys = { appSettings: "appSettings" }; -let appSettings = MMKV.getString(StorageKeys.appSettings); -if (appSettings) { - appSettings = JSON.parse(appSettings); +let appSettingsJson = MMKV.getString(StorageKeys.appSettings); +let appSettings: SettingStore["settings"] | null = null; +if (appSettingsJson) { + appSettings = JSON.parse(appSettingsJson) as SettingStore["settings"]; } const systemColorScheme = Appearance.getColorScheme(); @@ -50,34 +57,48 @@ const currentColorScheme = useSystemTheme ? systemColorScheme : appColorScheme; const theme = currentColorScheme === "dark" ? appSettings?.darkTheme - : appSettings?.lightTheme; + : appSettings?.lighTheme; const currentTheme = theme || (currentColorScheme === "dark" ? ThemeDark : ThemeLight); useThemeEngineStore.getState().setTheme(currentTheme); -export const useShareStore = create((set) => ({ +export type ShareStore = { + theme: ThemeDefinition; + appendNote: string | null; + selectedTags: string[]; + selectedNotebooks: string[]; + setAppendNote: (noteId: string | null) => void; + restore: () => void; + setSelectedNotebooks: (selectedNotebooks: string[]) => void; + setSelectedTags: (selectedTags: string[]) => void; +}; +export const useShareStore = create((set) => ({ theme: currentTheme, appendNote: null, setAppendNote: (noteId) => { - MMKV.setItem(StorageKeys.appendNote, noteId); + if (!noteId) { + MMKV.removeItem(StorageKeys.appendNote); + } else { + MMKV.setItem(StorageKeys.appendNote, noteId); + } set({ appendNote: noteId }); }, restore: () => { let appendNote = MMKV.getString(StorageKeys.appendNote); let selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks); let selectedTags = MMKV.getString(StorageKeys.selectedTag); - appendNote = JSON.parse(appendNote); + appendNote = appendNote; set({ appendNote: appendNote, selectedNotebooks: selectedNotebooks ? JSON.parse(selectedNotebooks) : [], - selectedTag: selectedTags ? JSON.parse(selectedTags) : [] + selectedTags: selectedTags ? JSON.parse(selectedTags) : [] }); }, selectedTags: [], selectedNotebooks: [], - setSelectedNotebooks: (selectedNotebooks) => { + setSelectedNotebooks: (selectedNotebooks: string[]) => { MMKV.setItem( StorageKeys.selectedNotebooks, JSON.stringify(selectedNotebooks) diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index d07fc8577..a4f476bf8 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -14,7 +14,7 @@ "@ammarahmed/react-native-background-fetch": "^4.2.2", "@ammarahmed/react-native-eventsource": "1.1.0", "@ammarahmed/react-native-fingerprint-scanner": "^5.0.1", - "@ammarahmed/react-native-share-extension": "^2.9.1", + "@ammarahmed/react-native-share-extension": "^2.9.3", "@ammarahmed/react-native-sodium": "^1.6.8", "@azure/core-asynciterator-polyfill": "^1.0.2", "@bam.tech/react-native-image-resizer": "3.0.11", @@ -518,9 +518,9 @@ } }, "node_modules/@ammarahmed/react-native-share-extension": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.1.tgz", - "integrity": "sha512-9ke3x9orQYb/3h13Cuk21T5YVM1cyLwDLndF22o7/ugFGAVA8VozpoFjHKlr3A6gKnp4m43zEkYfegHvUBj/gQ==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.3.tgz", + "integrity": "sha512-T7PgFydfcuOWjslTtM7qJMeI9vfQEiJSxavfISRdnguHlvxO1+sttAft19srpGAlw6HH3XXe6D5iioJ2xr7lkA==", "license": "MIT", "dependencies": { "react-native": "^0.63.1" diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3f9730c17..e169766e8 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -30,7 +30,7 @@ "@ammarahmed/react-native-background-fetch": "^4.2.2", "@ammarahmed/react-native-eventsource": "1.1.0", "@ammarahmed/react-native-fingerprint-scanner": "^5.0.1", - "@ammarahmed/react-native-share-extension": "^2.9.1", + "@ammarahmed/react-native-share-extension": "^2.9.3", "@ammarahmed/react-native-sodium": "^1.6.8", "@azure/core-asynciterator-polyfill": "^1.0.2", "@bam.tech/react-native-image-resizer": "3.0.11", @@ -85,6 +85,7 @@ "phone": "^3.1.14", "qclone": "^1.2.0", "react": "19.1.1", + "react-async-hook": "^4.0.0", "react-native": "0.82.0", "react-native-actions-sheet": "0.9.7", "react-native-actions-shortcuts": "^1.0.1", @@ -145,8 +146,7 @@ "toggle-switch-react-native": "3.2.0", "url": "^0.11.0", "validator": "^13.5.2", - "zustand": "^4.5.5", - "react-async-hook": "^4.0.0" + "zustand": "^4.5.5" }, "devDependencies": { "@babel/core": "^7.27.1",