/* 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 . */ import ShareExtension from "@ammarahmed/react-native-share-extension"; import { getPreviewData } from "@flyerhq/react-native-link-preview"; import { formatBytes, isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common"; import { isImage } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, Alert, Dimensions, Image, Keyboard, Platform, SafeAreaView, ScrollView, Text, TextInput, TouchableOpacity, View, useWindowDimensions } from "react-native"; import RNFetchBlob from "react-native-blob-util"; import { SafeAreaProvider } from "react-native-safe-area-context"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import isURL from "validator/lib/isURL"; import { DatabaseLogger, db } from "../app/common/database"; import { Storage } from "../app/common/database/storage"; import { Button } from "../app/components/ui/button"; import Heading from "../app/components/ui/typography/heading"; import Paragraph from "../app/components/ui/typography/paragraph"; import { useDBItem } from "../app/hooks/use-db-item"; import { eSendEvent } from "../app/services/event-manager"; import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../app/utils/constants"; import { eOnLoadNote } from "../app/utils/events"; import { NoteBundle } from "../app/utils/note-bundle"; import { defaultBorderRadius, AppFontSize } from "../app/utils/size"; import { AddNotebooks } from "./add-notebooks"; import { AddTags } from "./add-tags"; import { Editor } from "./editor"; import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview"; import { Search } from "./search"; import { initDatabase, useShareStore } from "./store"; const getLinkPreview = (url) => { return getPreviewData(url, 5000); }; async function sanitizeHtml(site) { try { let html = await fetchHandle.current?.processUrl(site); return html; } catch (e) { return ""; } } function makeHtmlFromUrl(url) { return `${url}`; } function makeHtmlFromPlainText(text) { if (!text) return ""; return `

${text .replace(/[\n]+/g, "\n") .replace(/(?:\r\n|\r|\n)/g, "

")}

`; } let defaultNote = { title: null, id: null, content: { type: "tiptap", data: null } }; const modes = { 1: { type: "text", title: "Plain text", icon: "card-text-outline" }, 2: { type: "clip", title: "Web clip", icon: "web" }, 3: { type: "link", title: "Link", icon: "link" } }; const ShareView = () => { const { colors } = useThemeColors(); const appendNoteId = useShareStore((state) => state.appendNote); const [note, setNote] = useState({ ...defaultNote }); const noteContent = useRef(""); const noteTitle = useRef(""); 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 [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 [kh, setKh] = useState(0); const [compress, setCompress] = useState(true); globalThis["IS_SHARE_EXTENSION"] = true; const onKeyboardDidShow = (event) => { let height = Dimensions.get("screen").height - event.endCoordinates.screenY; keyboardHeight.current = height; setKh(height); }; const onKeyboardDidHide = () => { keyboardHeight.current = 0; setKh(0); }; useEffect(() => { let keyboardDidShow = Keyboard.addListener( "keyboardDidShow", onKeyboardDidShow ); let keyboardDidHide = Keyboard.addListener( "keyboardDidHide", onKeyboardDidHide ); return () => { keyboardDidShow?.remove(); keyboardDidHide?.remove(); }; }, []); useEffect(() => { if (!fullQualityImages?.isAllowed) { setCompress(true); } }, [fullQualityImages]); const showLinkPreview = async (note, link) => { let _note = note; _note.content.data = makeHtmlFromUrl(link); try { let preview = await getLinkPreview(link); _note.title = preview.title; } catch (e) { console.log(e); } return note; }; const loadData = useCallback( async (isEditor) => { try { if (noteContent.current) { onLoad(); return; } defaultNote.content.data = null; setNote({ ...defaultNote }); const data = await ShareExtension.data(); if (!data || data.length === 0) { setRawData({ value: "" }); if (isEditor) { setTimeout(() => { editorRef.current?.focus(); }, 300); } return; } let note = { ...defaultNote }; for (let item of data) { if (item.type === "text") { setRawData(item); if (isURL(item.value)) { note = await showLinkPreview(note, item.value); } else { note.content.data = makeHtmlFromPlainText(item.value); } noteContent.current = note.content.data; } else if (item.type === "extras") { for (const key in item) { if (!key) continue; if (key.includes("TITLE") || key.includes("SUBJECT")) { note.title = item[key]; noteTitle.current = note.title; inputRef.current?.setNativeProps?.({ text: noteTitle.current }); } if (key.includes("TEXT") && !note.content.data) { note.content.data = item[key]; noteContent.current = item[key]; } } } else { const user = await db.user.getUser(); if (user) { const feature = await isFeatureAvailable( "fileSize", item.size || 0 ); if (!feature.isAllowed) { continue; } setRawFiles((files) => { const index = files.findIndex( (file) => file.name === item.name ); if (index === -1) { files.push(item); return [...files]; } else { return files; } }); } } } onLoad(); setNote({ ...note }); } catch (e) { console.error(e); } }, [onLoad] ); const onLoad = useCallback(() => { eSendEvent(eOnLoadNote + "shareEditor", { id: null, content: { type: "tiptap", data: noteContent.current }, forced: true }); }, []); useEffect(() => { (async () => { try { await initDatabase(); setLoadingExtension(false); loadData(); useShareStore.getState().restore(); } catch (e) { DatabaseLogger.error(e); } })(); }, [loadData]); const close = async () => { setNote({ ...defaultNote }); setLoadingExtension(true); ShareExtension.close(); }; const onPress = async () => { setLoading(true); if (!noteContent.current && rawFiles.length === 0 && !noteTitle.current) { setLoading(false); return; } let noteData; if (appendNoteId) { if (!(await db.notes.exists(appendNoteId))) { 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); noteData = { content: { data: (rawContent?.data || "") + "
" + noteContent.current, type: "tiptap" }, id: note.id, sessionId: Date.now() }; } else { noteData = { ...note }; noteData.content.data = noteContent.current; noteData.sessionId = Date.now(); noteData.title = noteTitle.current; } try { await NoteBundle.createNotes({ files: rawFiles, note: noteData, notebooks: useShareStore.getState().selectedNotebooks, tags: useShareStore.getState().selectedTags, compress }); if (!globalThis["IS_MAIN_APP_RUNNING"]) { await db.sync({ type: "send", force: false }); } else { console.log("main app running, skipping sync"); } } catch (e) { DatabaseLogger.error(e, "Error adding notes from share extension"); } await Storage.write("notesAddedFromIntent", "added"); close(); setLoading(false); }; const changeMode = async (m) => { setMode(m); setLoading(true); try { if (m === 2) { setLoadingPage(true); setTimeout(async () => { let html = await sanitizeHtml(rawData.value); noteContent.current = html; setLoadingPage(false); onLoad(); setNote((note) => { note.content.data = html; return { ...note }; }); }, 300); } else { setLoadingPage(false); let html = isURL(rawData.value) ? makeHtmlFromUrl(rawData.value) : makeHtmlFromPlainText(rawData.value); setNote((note) => { note.content.data = html; noteContent.current = html; onLoad(); return { ...note }; }); } } catch (e) { console.error(e); } finally { setLoading(false); } }; const onLoadEditor = useCallback(() => { Storage.write("shareExtensionOpened", "opened"); loadData(true); }, [loadData]); const onRemoveFile = (item) => { const index = rawFiles.findIndex((file) => file.name === item.name); if (index > -1) { setRawFiles((state) => { const files = [...state]; files.splice(index); return files; }); RNFetchBlob.fs.unlink(item.value).catch(console.log); } }; const WrapperView = Platform.OS === "android" ? View : ScrollView; return loadingExtension ? null : ( 500 ? 500 : width, height: height - kh, alignSelf: "center", justifyContent: "flex-end", overflow: "hidden" }} > {loadingPage ? : null} { if (searchMode) { setSearchMode(null); } else { close(); } }} style={{ width: "100%", height: "100%", position: "absolute" }} > {searchMode ? ( keyboardHeight.current} mode={searchMode} close={() => { setSearchMode(null); }} /> ) : null} {appendNoteId ? ( Save note ) : ( { noteTitle.current = value; }} defaultValue={noteTitle.current} blurOnSubmit={false} onSubmitEditing={() => { editorRef.current.focus(); }} /> )}