diff --git a/apps/mobile/.watchmanconfig b/.watchmanconfig similarity index 100% rename from apps/mobile/.watchmanconfig rename to .watchmanconfig diff --git a/apps/mobile/app/components/properties/index.js b/apps/mobile/app/components/properties/index.js index 915808966..e4c28a36b 100644 --- a/apps/mobile/app/components/properties/index.js +++ b/apps/mobile/app/components/properties/index.js @@ -74,6 +74,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { maxHeight: "100%" }} nestedScrollEnabled + bounces={false} data={[0]} keyExtractor={() => "properties-scroll-item"} renderItem={() => ( @@ -215,6 +216,8 @@ Properties.present = async (item, buttons = [], isSheet) => { "reminders", "local-only", "duplicate", + "copy-link", + "references", ...android, ...buttons ]); diff --git a/apps/mobile/app/components/properties/items.js b/apps/mobile/app/components/properties/items.js index a4d544b6d..e5adea536 100644 --- a/apps/mobile/app/components/properties/items.js +++ b/apps/mobile/app/components/properties/items.js @@ -45,7 +45,6 @@ export const Items = ({ item, buttons, close }) => { const _renderRowItem = ({ item }) => ( { ); const renderTopBarItem = (item, index) => { - const isLast = index === topBarItems.length; return ( { activeOpacity={1} style={{ alignSelf: "flex-start", - marginRight: isLast ? 0 : 10, paddingHorizontal: 0, width: topBarItemWidth }} @@ -194,6 +191,8 @@ export const Items = ({ item, buttons, close }) => { "history", "reminders", "attachments", + "references", + "copy-link", "trash" ]; @@ -227,6 +226,9 @@ export const Items = ({ item, buttons, close }) => { paddingHorizontal: 12, marginTop: 6 }} + contentContainerStyle={{ + gap: 10 + }} > {topBarItems.map(renderTopBarItem)} diff --git a/apps/mobile/app/components/sheets/link-note/index.tsx b/apps/mobile/app/components/sheets/link-note/index.tsx index d63e43227..8f3a04dc5 100644 --- a/apps/mobile/app/components/sheets/link-note/index.tsx +++ b/apps/mobile/app/components/sheets/link-note/index.tsx @@ -16,7 +16,12 @@ 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 { ContentBlock, Note, VirtualizedGrouping } from "@notesnook/core"; +import { + ContentBlock, + Note, + VirtualizedGrouping, + createInternalLink +} from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { useEffect, useRef, useState } from "react"; import { TextInput, View } from "react-native"; @@ -25,10 +30,12 @@ import { db } from "../../../common/database"; import { useDBItem } from "../../../hooks/use-db-item"; import { presentSheet } from "../../../services/event-manager"; import { SIZE } from "../../../utils/size"; +import { Button } from "../../ui/button"; import Input from "../../ui/input"; import { PressableButton } from "../../ui/pressable"; import Paragraph from "../../ui/typography/paragraph"; -import { Button } from "../../ui/button"; +import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link"; +import { editorController } from "../../../screens/editor/tiptap/utils"; const ListNoteItem = ({ id, @@ -91,10 +98,25 @@ const ListBlockItem = ({ style={{ flexDirection: "row", width: "100%", - justifyContent: "space-between", + columnGap: 10, alignItems: "center" }} > + + + {item.type.toUpperCase()} + + + - - - - {item.type.toUpperCase()} - - ); }; -export default function LinkNote() { +export default function LinkNote(props: { + attributes: LinkAttributes; + resolverId: string; + onLinkCreated: () => void; + close?: (ctx?: string) => void; +}) { const { colors } = useThemeColors(); const query = useRef(); const [notes, setNotes] = useState>(); @@ -141,6 +153,7 @@ export default function LinkNote() { setNotes(notes); }); }, []); + console.log(new URL("https://google.com").protocol); const onChange = async (value: string) => { query.current = value; @@ -161,6 +174,26 @@ export default function LinkNote() { } }; + const onCreateLink = (blockId?: string) => { + if (!selectedNote) return; + const link = createInternalLink( + "note", + selectedNote.id, + blockId + ? { + blockId: blockId + } + : undefined + ); + editorController.current.commands.createInternalLink( + { + href: link, + title: selectedNote.title + }, + props.resolverId + ); + }; + const onSelectNote = async (note: Note) => { setSelectedNote(note); inputRef.current?.clear(); @@ -172,17 +205,16 @@ export default function LinkNote() { }; const onSelectBlock = (block: ContentBlock) => { - setSelectedNodeId(block.id); + onCreateLink(block.id); + props.onLinkCreated(); + props.close?.(); }; return ( ( )} + style={{ + marginTop: 10 + }} keyExtractor={(item) => item.id} data={nodes} /> @@ -282,6 +317,9 @@ export default function LinkNote() { onSelectNote={onSelectNote} /> )} + style={{ + marginTop: 10 + }} data={notes?.placeholders} /> )} @@ -294,14 +332,36 @@ export default function LinkNote() { title="Create link" type="accent" width="100%" + onPress={() => { + onCreateLink(); + props.onLinkCreated(); + props.close?.(); + }} /> ) : null} ); } -LinkNote.present = () => { +LinkNote.present = (attributes: LinkAttributes, resolverId: string) => { + let didCreateLink = false; presentSheet({ - component: () => + component: (ref, close) => ( + { + didCreateLink = true; + }} + close={close} + /> + ), + onClose: () => { + if (!didCreateLink) { + editorController?.current.commands.dismissCreateInternalLinkRequest( + resolverId + ); + } + } }); }; diff --git a/apps/mobile/app/components/sheets/references/index.tsx b/apps/mobile/app/components/sheets/references/index.tsx new file mode 100644 index 000000000..c4f1598a2 --- /dev/null +++ b/apps/mobile/app/components/sheets/references/index.tsx @@ -0,0 +1,521 @@ +/* +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 { + InternalLink, + TextSlice, + VirtualizedGrouping, + createInternalLink, + highlightInternalLinks +} from "@notesnook/core"; +import { ContentBlock, ItemReference, Note } from "@notesnook/core/dist/types"; +import { useThemeColors } from "@notesnook/theme"; +import React, { MutableRefObject, useEffect, useRef, useState } from "react"; +import { ActivityIndicator, View } from "react-native"; +import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; +import create from "zustand"; +import { db } from "../../../common/database"; +import { useDBItem } from "../../../hooks/use-db-item"; +import { eSendEvent, presentSheet } from "../../../services/event-manager"; +import { useRelationStore } from "../../../stores/use-relation-store"; +import { eOnLoadNote } from "../../../utils/events"; +import { tabBarRef } from "../../../utils/global-refs"; +import { SIZE } from "../../../utils/size"; +import SheetProvider from "../../sheet-provider"; +import { Button } from "../../ui/button"; +import { IconButton } from "../../ui/icon-button"; +import { PressableButton } from "../../ui/pressable"; +import Paragraph from "../../ui/typography/paragraph"; + +export const useExpandedStore = create<{ + expanded: { + [id: string]: boolean; + }; + setExpanded: (id: string) => void; +}>((set, get) => ({ + expanded: {}, + setExpanded(id: string) { + set({ + expanded: { + ...get().expanded, + [id]: !get().expanded[id] + } + }); + } +})); + +const ListBlockItem = ({ + item, + onSelectBlock +}: { + item: ContentBlock; + onSelectBlock: () => void; +}) => { + const { colors } = useThemeColors(); + return ( + { + onSelectBlock(); + }} + type={"transparent"} + customStyle={{ + flexDirection: "row", + width: "100%", + paddingLeft: 35, + justifyContent: "flex-start", + minHeight: 45, + paddingHorizontal: 12 + }} + > + + + + {item.type.toUpperCase()} + + + + {item?.content.length > 200 + ? item?.content.slice(0, 200) + "..." + : item.content} + + + + ); +}; + +const ListNoteInternalLink = ({ + link, + onSelect +}: { + link: { + blockId: string; + highlightedText: [TextSlice, TextSlice, TextSlice][]; + }; + onSelect: () => void; +}) => { + const { colors } = useThemeColors(); + return ( + { + onSelect(); + }} + type={"transparent"} + customStyle={{ + flexDirection: "row", + width: "100%", + paddingLeft: 35, + justifyContent: "flex-start", + minHeight: 45 + }} + > + + {link.highlightedText.map((text) => ( + + {text.map((slice) => + !slice.highlighted ? ( + slice.text + ) : ( + + {slice.text} + + ) + )} + + ))} + + + ); +}; + +const ListNoteItem = ({ + id, + items, + onSelect, + reference, + internalLinks, + listType +}: { + id: number; + items: VirtualizedGrouping | undefined; + onSelect: (item: Note, blockId?: string) => void; + reference: Note; + internalLinks: MutableRefObject[] | undefined>; + listType: "linkedNotes" | "referencedIn"; +}) => { + const { colors } = useThemeColors(); + const [item] = useDBItem(id, "note", items); + const expanded = useExpandedStore((state) => + !item ? false : state.expanded[item.id] + ); + const [linkedBlocks, setLinkedBlocks] = useState([]); + const [noteInternalLinks, setNoteInternalLinks] = useState< + { + blockId: string; + highlightedText: [TextSlice, TextSlice, TextSlice][]; + }[] + >([]); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (item?.id && expanded) { + (async () => { + if (listType === "linkedNotes") { + if (linkedBlocks.length) return; + setLoading(true); + if (!internalLinks.current) { + internalLinks.current = await db.notes.internalLinks(reference.id); + } + const noteLinks = internalLinks.current.filter( + (link) => link.id === item.id && link.params?.blockId + ); + + if (noteLinks.length) { + const blocks = await db.notes.contentBlocks(item.id); + + setLinkedBlocks( + blocks.filter((block) => + noteLinks.find((link) => block.id === link.params?.blockId) + ) + ); + } + } else { + if (noteInternalLinks.length) return; + setLoading(true); + const blocks = await db.notes.contentBlocks(item.id); + + setNoteInternalLinks( + blocks + .filter((block) => + block.content.includes(createInternalLink("note", reference.id)) + ) + .map((block) => { + return { + blockId: block?.id as string, + highlightedText: highlightInternalLinks( + block as ContentBlock, + reference.id + ) + }; + }) + ); + } + + setLoading(false); + })(); + } + }, [ + item?.id, + expanded, + linkedBlocks.length, + internalLinks, + reference.id, + listType, + noteInternalLinks.length + ]); + + const renderBlock = React.useCallback( + (block: ContentBlock) => ( + { + if (!item) return; + onSelect(item, block.id); + }} + /> + ), + [item, onSelect] + ); + + const renderInternalLink = React.useCallback( + (link: { + blockId: string; + highlightedText: [TextSlice, TextSlice, TextSlice][]; + }) => ( + { + if (!item) return; + onSelect(item, link.blockId); + }} + /> + ), + [item, onSelect] + ); + + return ( + + { + if (!item) return; + onSelect(item as Note); + }} + customStyle={{ + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-start", + width: "100%", + height: 45 + }} + > + { + if (!item?.id) return; + useExpandedStore.getState().setExpanded(item?.id); + }} + top={0} + left={0} + bottom={0} + right={0} + customStyle={{ + width: 35, + height: 35 + }} + name={expanded ? "chevron-down" : "chevron-right"} + /> + {item?.title} + + + {expanded && !item?.locked ? ( + + {loading ? ( + + ) : ( + <> + {listType === "linkedNotes" ? ( + <> + {linkedBlocks.length === 0 ? ( + + No blocks linked + + ) : ( + linkedBlocks.map(renderBlock) + )} + + ) : ( + <> + {noteInternalLinks.length === 0 ? ( + + No references found of this note + + ) : ( + noteInternalLinks.map(renderInternalLink) + )} + + )} + + )} + + ) : null} + + ); +}; + +type ReferencesListProps = { + item: { id: string; type: string }; + close?: (ctx?: any) => void; +}; + +export const ReferencesList = ({ item, close }: ReferencesListProps) => { + const [tab, setTab] = useState(0); + const updater = useRelationStore((state) => state.updater); + const { colors } = useThemeColors(); + const [items, setItems] = useState>(); + const hasNoRelations = !items || items?.placeholders?.length === 0; + const internalLinks = useRef[]>(); + + useEffect(() => { + db.relations?.[tab === 0 ? "from" : "to"]?.( + { id: item?.id, type: item?.type } as ItemReference, + "note" + ) + .selector.sorted({ + sortBy: "dateEdited", + sortDirection: "desc" + }) + .then((items) => { + setItems(items); + }); + }, [item?.id, item?.type, tab]); + + const renderNote = React.useCallback( + ({ index }: any) => ( + { + console.log(note.id, blockId); + eSendEvent(eOnLoadNote, { + item: note, + blockId: blockId + }); + tabBarRef.current?.goToPage(1); + close?.(); + }} + reference={item as Note} + internalLinks={internalLinks} + listType={tab === 0 ? "linkedNotes" : "referencedIn"} + /> + ), + [items, item, tab, close] + ); + + return ( + + + + + - - {!settings.premium && ( )} - - {settings.deviceMode !== "mobile" && !settings.fullscreen ? ( + + + + + + void }; /** * Id of current session */ @@ -187,9 +189,16 @@ export const EventTypes = { tabsChanged: "editor-events:tabs-changed", showTabs: "editor-events:show-tabs", tabFocused: "editor-events:tab-focused", - toc: "editor-events:toc" + toc: "editor-events:toc", + createInternalLink: "editor-events:create-internal-link" } as const; +export function randId(prefix: string) { + return Math.random() + .toString(36) + .replace("0.", prefix || ""); +} + export function isReactNative(): boolean { return !!window.ReactNativeWebView; } diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 9cf484e2e..0fe0b9519 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -105,6 +105,9 @@ "@streetwriters/showdown": "^3.0.5-alpha", "async-mutex": "^0.3.2", "dayjs": "1.11.9", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", "entities": "^4.3.1", "fuzzyjs": "^5.0.1", "html-to-text": "^9.0.5",