/* 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 { getPreviewData } from "@flyerhq/react-native-link-preview"; import { parseHTML } from "@notesnook/core/utils/html-parser"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, Alert, Keyboard, Platform, SafeAreaView, ScrollView, StatusBar, Text, TouchableOpacity, View, useWindowDimensions } from "react-native"; import { SafeAreaProvider, useSafeAreaInsets } from "react-native-safe-area-context"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import ShareExtension from "rn-extensions-share"; 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 { getElevation } from "../app/utils"; import { eOnLoadNote } from "../app/utils/events"; import { Editor } from "./editor"; import { Search } from "./search"; import { initDatabase, useShareStore } from "./store"; 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( /(?:\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 = useShareStore((state) => state.colors); const accent = useShareStore((state) => state.accent); 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 [kh, setKh] = useState(0); const onKeyboardDidShow = (event) => { let kHeight = event.endCoordinates.height; keyboardHeight.current = kHeight; setKh(kHeight); }; const onKeyboardDidHide = () => { keyboardHeight.current = 0; setKh(0); }; useEffect(() => { useShareStore.getState().setAccent(); 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; } } 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 loadDatabase(); 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); await initDatabase(); if (!noteContent.current) 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(); } let id = await db.notes.add(_note); if (!appendNote) { for (const item of useShareStore.getState().selectedNotebooks) { if (item.type === "notebook") { db.relations.add(item, { id, type: "note" }); } else { db.notes.addToNotebook( { id: item.notebookId, topic: item.id }, id ); } } } await db.sync(false, false); await Storage.write("notesAddedFromIntent", "added"); close(); setLoading(false); }; useEffect(() => { useShareStore.getState().setColors(); }, [note]); 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 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 ? (