/* 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 } from "@notesnook/common"; import { isImage } from "@notesnook/core/utils/filename"; import { parseHTML } from "@notesnook/core/utils/html-parser"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, Alert, Image, Keyboard, Platform, SafeAreaView, ScrollView, StatusBar, Text, TouchableOpacity, View, useWindowDimensions } from "react-native"; import RNFetchBlob from "react-native-blob-util"; import { SafeAreaProvider, useSafeAreaInsets } from "react-native-safe-area-context"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import isURL from "validator/lib/isURL"; import { db } from "../app/common/database"; import Storage from "../app/common/database/storage"; import { eSendEvent } from "../app/services/event-manager"; import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../app/utils/constants"; import { getElevationStyle } from "../app/utils/elevation"; import { eOnLoadNote } from "../app/utils/events"; import { NoteBundle } from "../app/utils/note-bundle"; import { Editor } from "./editor"; import { Search } from "./search"; import { initDatabase, useShareStore } from "./store"; import { useThemeColors } from "@notesnook/theme"; const getLinkPreview = (url) => { return getPreviewData(url, 5000); }; async function sanitizeHtml(site) { try { let html = await fetch(site); html = await html.text(); return sanitize(html, site); } 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, "

")}

`; } function getBaseUrl(site) { var url = site.split("/").slice(0, 3).join("/"); return url; } function wrapTablesWithDiv(document) { const tables = document.getElementsByTagName("table"); for (let table of tables) { table.setAttribute("contenteditable", "true"); const div = document.createElement("div"); div.setAttribute("contenteditable", "false"); div.innerHTML = table.outerHTML; div.classList.add("table-container"); table.replaceWith(div); } return document; } let elementBlacklist = [ "script", "button", "input", "textarea", "style", "form", "link", "head", "nav", "iframe", "canvas", "select", "dialog", "footer" ]; function removeInvalidElements(document) { let elements = document.querySelectorAll(elementBlacklist.join(",")); for (let element of elements) { element.remove(); } return document; } function replaceSrcWithAbsoluteUrls(document, baseUrl) { let images = document.querySelectorAll("img"); for (var i = 0; i < images.length; i++) { let img = images[i]; let url = getBaseUrl(baseUrl); let src = img.getAttribute("src"); if (src.startsWith("/")) { if (src.startsWith("//")) { src = src.replace("//", "https://"); } else { src = url + src; } } if (src.startsWith("data:")) { img.remove(); } else { img.setAttribute("src", src); } } return document; } function fixCodeBlocks(document) { let elements = document.querySelectorAll("code,pre"); for (let element of elements) { element.classList.add(".hljs"); } return document; } function sanitize(html, baseUrl) { let parser = parseHTML(html); parser = wrapTablesWithDiv(parser); parser = removeInvalidElements(parser); parser = replaceSrcWithAbsoluteUrls(parser, baseUrl); parser = fixCodeBlocks(parser); let htmlString = parser.body.outerHTML; htmlString = htmlString + `
${makeHtmlFromUrl(baseUrl)}`; return htmlString; } 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 = ({ quicknote = false }) => { const { colors } = useThemeColors(); const appendNote = useShareStore((state) => state.appendNote); const [note, setNote] = useState({ ...defaultNote }); const noteContent = useRef(""); const [loading, setLoading] = useState(false); const [loadingExtension, setLoadingExtension] = useState(true); const [rawData, setRawData] = useState({ type: null, value: null }); const [mode, setMode] = useState(1); const keyboardHeight = useRef(0); const { width, height } = useWindowDimensions(); const insets = Platform.OS === "android" ? { top: StatusBar.currentHeight } : // eslint-disable-next-line react-hooks/rules-of-hooks useSafeAreaInsets(); const [searchMode, setSearchMode] = useState(null); const [rawFiles, setRawFiles] = useState([]); const [kh, setKh] = useState(0); globalThis["IS_SHARE_EXTENSION"] = true; const onKeyboardDidShow = (event) => { let kHeight = event.endCoordinates.height; keyboardHeight.current = kHeight; setKh(kHeight); }; 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(); }; }, []); 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 () => { try { defaultNote.content.data = null; setNote({ ...defaultNote }); const data = await ShareExtension.data(); if (!data || data.length === 0) { setRawData({ value: "" }); 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 { const user = await db.user.getUser(); if (user && user.subscription.type !== 0) { if ( (isImage(item.type) && item.size > IMAGE_SIZE_LIMIT) || (!isImage(item.type) && item.size > FILE_SIZE_LIMIT) ) continue; setRawFiles((files) => { const index = files.findIndex((file) => file.name === item.name); if (index === -1) { files.push(item); return [...files]; } else { return files; } }); } } } setNote({ ...note }); onLoad(); } catch (e) { console.error(e); } }, [onLoad]); const onLoad = useCallback(() => { eSendEvent(eOnLoadNote + "shareEditor", { id: null, content: { type: "tiptap", data: noteContent.current }, forced: true }); }, []); useEffect(() => { (async () => { await initDatabase(); setLoadingExtension(false); loadData(); useShareStore.getState().restore(); })(); }, [loadData]); const close = async () => { setNote({ ...defaultNote }); setLoadingExtension(true); if (quicknote) { ShareExtension.openURL("ShareMedia://MainApp"); } else { ShareExtension.close(); } }; const onPress = async () => { setLoading(true); if (!noteContent.current && rawFiles.length === 0) { setLoading(false); return; } if (appendNote && !db.notes.note(appendNote.id)) { useShareStore.getState().setAppendNote(null); Alert.alert("The note you are trying to append to has been deleted."); return; } let _note; if (appendNote && db.notes.note(appendNote.id)) { let raw = await db.content.raw(appendNote.contentId); _note = { content: { data: (raw?.data || "") + noteContent.current, type: "tiptap" }, id: appendNote.id, sessionId: Date.now() }; } else { _note = { ...note }; _note.tags = useShareStore.getState().selectedTags || []; _note.content.data = noteContent.current; _note.sessionId = Date.now(); } await NoteBundle.createNotes({ files: rawFiles, note: _note, notebooks: useShareStore.getState().selectedNotebooks }); try { await db.sync(false, false); } catch (e) { console.log(e, e.stack); } await Storage.write("notesAddedFromIntent", "added"); close(); setLoading(false); }; const changeMode = async (m) => { setMode(m); setLoading(true); try { if (m === 2) { let html = await sanitizeHtml(rawData.value); setNote((note) => { note.content.data = html; noteContent.current = html; onLoad(); return { ...note }; }); } else { 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(); }, [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: quicknote ? height : height - kh, alignSelf: "center", justifyContent: quicknote ? "flex-start" : "flex-end" }} > {quicknote && !searchMode ? (