mobile: finalize note linking

This commit is contained in:
Ammar Ahmed
2024-01-24 18:58:14 +05:00
committed by Abdullah Atta
parent ef6c4bb6fb
commit 09f37ae872
26 changed files with 6551 additions and 46846 deletions

View File

@@ -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
]);

View File

@@ -45,7 +45,6 @@ export const Items = ({ item, buttons, close }) => {
const _renderRowItem = ({ item }) => (
<View
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
style={{
@@ -117,7 +116,6 @@ export const Items = ({ item, buttons, close }) => {
);
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
return (
<Pressable
onPress={item.func}
@@ -126,7 +124,6 @@ export const Items = ({ item, buttons, close }) => {
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)}
</ScrollView>

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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"
}}
>
<View
style={{
borderRadius: 5,
backgroundColor: colors.secondary.background,
width: 25,
height: 25,
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{item.type.toUpperCase()}
</Paragraph>
</View>
<Paragraph
style={{
flexShrink: 1
@@ -104,27 +126,17 @@ const ListBlockItem = ({
? item?.content.slice(0, 200) + "..."
: item.content}
</Paragraph>
<View
style={{
borderRadius: 5,
backgroundColor: colors.secondary.background,
width: 30,
height: 30,
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{item.type.toUpperCase()}
</Paragraph>
</View>
</View>
</PressableButton>
);
};
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<string>();
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
@@ -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 (
<View
style={{
paddingHorizontal: 12,
height: "100%",
flexShrink: 1,
borderWidth: 2,
borderColor: "red"
minHeight: 400
}}
>
<View
@@ -270,6 +302,9 @@ export default function LinkNote() {
renderItem={({ item, index }) => (
<ListBlockItem item={item} onSelectBlock={onSelectBlock} />
)}
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}
</View>
);
}
LinkNote.present = () => {
LinkNote.present = (attributes: LinkAttributes, resolverId: string) => {
let didCreateLink = false;
presentSheet({
component: () => <LinkNote />
component: (ref, close) => (
<LinkNote
attributes={attributes}
resolverId={resolverId}
onLinkCreated={() => {
didCreateLink = true;
}}
close={close}
/>
),
onClose: () => {
if (!didCreateLink) {
editorController?.current.commands.dismissCreateInternalLinkRequest(
resolverId
);
}
}
});
};

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<PressableButton
onPress={() => {
onSelectBlock();
}}
type={"transparent"}
customStyle={{
flexDirection: "row",
width: "100%",
paddingLeft: 35,
justifyContent: "flex-start",
minHeight: 45,
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
columnGap: 10
}}
>
<View
style={{
borderRadius: 5,
backgroundColor: colors.secondary.background,
width: 25,
height: 25,
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{item.type.toUpperCase()}
</Paragraph>
</View>
<Paragraph
style={{
flexShrink: 1,
marginTop: 2
}}
>
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: item.content}
</Paragraph>
</View>
</PressableButton>
);
};
const ListNoteInternalLink = ({
link,
onSelect
}: {
link: {
blockId: string;
highlightedText: [TextSlice, TextSlice, TextSlice][];
};
onSelect: () => void;
}) => {
const { colors } = useThemeColors();
return (
<PressableButton
onPress={() => {
onSelect();
}}
type={"transparent"}
customStyle={{
flexDirection: "row",
width: "100%",
paddingLeft: 35,
justifyContent: "flex-start",
minHeight: 45
}}
>
<View
style={{
width: "100%",
rowGap: 10
}}
>
{link.highlightedText.map((text) => (
<Paragraph
key={`root_${text[0].text}`}
style={{
flexShrink: 1,
marginTop: 2,
flexWrap: "wrap"
}}
>
{text.map((slice) =>
!slice.highlighted ? (
slice.text
) : (
<Paragraph
key={slice.text}
color={colors.selected.accent}
style={{
textDecorationLine: "underline",
textDecorationColor: colors.selected.accent
}}
>
{slice.text}
</Paragraph>
)
)}
</Paragraph>
))}
</View>
</PressableButton>
);
};
const ListNoteItem = ({
id,
items,
onSelect,
reference,
internalLinks,
listType
}: {
id: number;
items: VirtualizedGrouping<Note> | undefined;
onSelect: (item: Note, blockId?: string) => void;
reference: Note;
internalLinks: MutableRefObject<InternalLink<"note">[] | 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<ContentBlock[]>([]);
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) => (
<ListBlockItem
item={block}
onSelectBlock={() => {
if (!item) return;
onSelect(item, block.id);
}}
/>
),
[item, onSelect]
);
const renderInternalLink = React.useCallback(
(link: {
blockId: string;
highlightedText: [TextSlice, TextSlice, TextSlice][];
}) => (
<ListNoteInternalLink
link={link}
onSelect={() => {
if (!item) return;
onSelect(item, link.blockId);
}}
/>
),
[item, onSelect]
);
return (
<View
style={{
flexDirection: "column",
width: "100%",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<PressableButton
type={"gray"}
onPress={() => {
if (!item) return;
onSelect(item as Note);
}}
customStyle={{
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
height: 45
}}
>
<IconButton
size={SIZE.xl}
onPress={() => {
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"}
/>
<Paragraph numberOfLines={1}>{item?.title}</Paragraph>
</PressableButton>
{expanded && !item?.locked ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
width: "100%"
}}
>
{loading ? (
<ActivityIndicator color={colors.primary.accent} size={SIZE.lg} />
) : (
<>
{listType === "linkedNotes" ? (
<>
{linkedBlocks.length === 0 ? (
<Paragraph color={colors.secondary.paragraph}>
No blocks linked
</Paragraph>
) : (
linkedBlocks.map(renderBlock)
)}
</>
) : (
<>
{noteInternalLinks.length === 0 ? (
<Paragraph color={colors.secondary.paragraph}>
No references found of this note
</Paragraph>
) : (
noteInternalLinks.map(renderInternalLink)
)}
</>
)}
</>
)}
</View>
) : null}
</View>
);
};
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<VirtualizedGrouping<Note>>();
const hasNoRelations = !items || items?.placeholders?.length === 0;
const internalLinks = useRef<InternalLink<"note">[]>();
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) => (
<ListNoteItem
id={index}
items={items}
onSelect={(note, blockId) => {
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 (
<View style={{ height: "100%" }}>
<SheetProvider context="local" />
<View
style={{
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border
}}
>
<Button
type={"gray"}
title="Linked notes"
style={{
borderRadius: 0,
borderBottomWidth: 3,
borderColor: tab === 0 ? colors.primary.accent : "transparent",
height: 40,
width: "50%"
}}
onPress={() => {
setTab(0);
}}
/>
<Button
type={"gray"}
title="Referenced in"
style={{
width: "50%",
borderRadius: 0,
borderBottomWidth: 3,
borderColor: tab === 1 ? colors.primary.accent : "transparent",
height: 40
}}
onPress={() => {
setTab(1);
}}
/>
</View>
{hasNoRelations ? (
<View
style={{
height: "85%",
justifyContent: "center",
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{tab === 1
? "This note is not referenced in other notes."
: "This note does not link to other notes."}
</Paragraph>
</View>
) : (
<View
style={{
paddingHorizontal: 12,
flex: 1,
marginTop: 10
}}
>
<FlashList
bounces={false}
data={items.placeholders}
renderItem={renderNote}
/>
</View>
)}
</View>
);
};
ReferencesList.present = ({
reference
}: {
reference: { id: string; type: string };
}) => {
presentSheet({
component: (ref, close, update) => (
<ReferencesList item={reference} close={close} />
),
onClose: () => {
useExpandedStore.setState({
expanded: {}
});
}
});
};

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { VAULT_ERRORS } from "@notesnook/core/dist/api/vault";
import {
Color,
ItemReference,
Note,
Notebook,
Reminder,
@@ -64,6 +65,7 @@ import { eOpenLoginDialog } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { ReferencesList } from "../components/sheets/references";
export const useActions = ({
close,
@@ -801,7 +803,20 @@ export const useActions = ({
icon: "history",
func: openHistory
},
{
id: "copy-link",
title: "Copy link",
icon: "link",
func: () => {
Clipboard.setString(`nn://note/${item.id}`);
ToastManager.show({
heading: "Note link copied",
message: `nn://note/${item.id}`,
context: "local",
type: "success"
});
}
},
{
id: "reminders",
title: "Reminders",
@@ -909,6 +924,16 @@ export const useActions = ({
title: "Add tags",
icon: "pound",
func: addTo
},
{
id: "references",
title: "References",
icon: "vector-link",
func: () => {
ReferencesList.present({
reference: item as ItemReference
});
}
}
);
}

View File

@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
* The url should be something like this: http://192.168.100.126:3000/index.html
*/
export const EDITOR_URI = __DEV__
? "http://192.168.43.252:3000/index.html"
? EditorMobileSourceUrl
: EditorMobileSourceUrl;

View File

@@ -29,6 +29,7 @@ import { Settings } from "./types";
import { getResponse, randId, textInput } from "./utils";
import { Note } from "@notesnook/core/dist/types";
import { useTabStore } from "./use-tab-store";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
type Action = { job: string; id: string };
@@ -322,6 +323,28 @@ const image = toBlobURL("${image.dataurl}", "${image.hash}");
`);
};
createInternalLink = async (
attributes: LinkAttributes,
resolverId: string
) => {
if (!resolverId) return;
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](${JSON.stringify(
attributes
)});
}`);
};
dismissCreateInternalLinkRequest = async (resolverId: string) => {
if (!resolverId) return;
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](undefined);
}
`);
};
scrollIntoViewById = async (id: string) => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`

View File

@@ -41,5 +41,6 @@ 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"
};

View File

@@ -71,6 +71,8 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState } from "./utils";
import TableOfContents from "../../../components/sheets/toc";
import LinkNote from "../../../components/sheets/link-note";
import { parseInternalLink } from "@notesnook/core";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -504,7 +506,35 @@ export const useEditorEvents = (
eSendEvent(eOpenFullscreenEditor);
break;
case EventTypes.link:
openLinkInBrowser(editorMessage.value as string);
if (editorMessage.value.startsWith("nn://")) {
const data = parseInternalLink(editorMessage.value);
if (!data?.id) break;
if (
data.id ===
useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {
if (!data.params?.blockId) return;
editor.commands.scrollIntoViewById(data.params.blockId);
}, 150);
}
return;
}
eSendEvent(eOnLoadNote, {
item: await db.notes.note(data?.id),
blockId: data.params?.blockId
});
console.log(
"Opening note from internal link:",
editorMessage.value
);
} else {
openLinkInBrowser(editorMessage.value as string);
}
break;
case EventTypes.previewAttachment: {
@@ -576,6 +606,13 @@ export const useEditorEvents = (
break;
}
case EventTypes.createInternalLink: {
LinkNote.present(
editorMessage.value.attributes,
editorMessage.value.resolverId
);
break;
}
default:
break;

View File

@@ -132,6 +132,7 @@ export const useEditor = (
const currentLoadingNoteId = useRef<string>();
const loadingState = useRef<string>();
const lastTabFocused = useRef(0);
const blockIdRef = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
await post(
@@ -382,7 +383,9 @@ export const useEditor = (
forced?: boolean;
newNote?: boolean;
tabId?: number;
blockId?: string;
}) => {
blockIdRef.current = event.blockId;
state.current.currentlyEditing = true;
if (
!state.current.ready &&
@@ -495,6 +498,13 @@ export const useEditor = (
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
loadingState.current = undefined;
await commands.setTags(item);
commands.setSettings();
@@ -743,7 +753,7 @@ export const useEditor = (
restoreTabNote();
}
}
});
}, 500);
}, [
onReady,
postMessage,

View File

@@ -29,6 +29,7 @@ import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNotes } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common";
import LinkNote from "../../components/sheets/link-note";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
@@ -79,7 +80,12 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
loading: "Loading your notes"
}}
/>
<FloatingButton title="Create a new note" onPress={openEditor} />
<FloatingButton
title="Create a new note"
onPress={() => {
LinkNote.present();
}}
/>
</DelayLayout>
</>
);

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import 'react-native-url-polyfill/auto';
import "./polyfills/console-time.js"
global.Buffer = require('buffer').Buffer;
import '../app/common/logger/index';

View File

@@ -66,7 +66,8 @@
"react-native-quick-sqlite": "^8.0.6",
"react-native-theme-switch-animation": "^0.6.0",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"react-native-image-crop-picker": "^0.40.2"
"react-native-image-crop-picker": "^0.40.2",
"react-native-url-polyfill": "^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,9 @@
"@notesnook/editor": "file:../../packages/editor",
"@notesnook/editor-mobile": "file:../../packages/editor-mobile",
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
"react": "18.2.0",
"react-native": "0.72.0"
}
}
}

View File

@@ -74,7 +74,8 @@ const EXTRA_ICON_NAMES = [
"cloud",
"restore",
"keyboard",
"numeric"
"numeric",
"vector-link"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -977,7 +977,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.2-beta",
"version": "3.0.6-beta",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -1020,6 +1020,7 @@
"cronosjs": "^1.7.1",
"date-fns": "^2.30.0",
"dayjs": "1.11.9",
"diffblazer": "^1.0.1",
"electron-trpc": "0.5.2",
"event-source-polyfill": "^1.0.25",
"fflate": "^0.8.0",
@@ -1038,11 +1039,13 @@
"platform": "^1.3.6",
"qclone": "^1.2.0",
"react": "18.2.0",
"react-avatar-editor": "^13.0.2",
"react-complex-tree": "^2.2.4",
"react-day-picker": "^8.9.1",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.12",
"react-freeze": "^1.0.3",
"react-hot-toast": "^2.4.1",
"react-loading-skeleton": "^3.3.1",
"react-modal": "3.16.1",
@@ -1068,8 +1071,10 @@
"@types/node-fetch": "^2.5.10",
"@types/platform": "^1.3.4",
"@types/react": "^18.2.39",
"@types/react-avatar-editor": "^13.0.2",
"@types/react-dom": "^18.2.17",
"@types/react-modal": "3.16.3",
"@types/react-scroll-sync": "^0.9.0",
"@types/tinycolor2": "^1.4.3",
"@types/wicg-file-system-access": "^2020.9.6",
"@vitejs/plugin-react-swc": "3.3.2",

View File

@@ -36,6 +36,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",
@@ -176,6 +179,9 @@
"bson-objectid": "^2.0.4",
"cross-env": "^7.0.3",
"dayjs": "1.11.9",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"dotenv": "^16.0.1",
"entities": "^4.3.1",
"event-source-polyfill": "^1.0.31",

View File

@@ -39,7 +39,7 @@ import {
useTabStore
} from "../hooks/useTabStore";
import { EmotionEditorToolbarTheme } from "../theme-factory";
import { EventTypes, Settings } from "../utils";
import { EventTypes, randId, Settings } from "../utils";
import Header from "./header";
import StatusBar from "./statusbar";
import Tags from "./tags";
@@ -82,15 +82,15 @@ const Tiptap = ({
transaction.getMeta("ignoreEdit")
);
},
onOpenAttachmentPicker: (editor, type) => {
openAttachmentPicker: (type) => {
globalThis.editorControllers[tab.id]?.openFilePicker(type);
return true;
},
onDownloadAttachment: (editor, attachment) => {
downloadAttachment: (attachment) => {
globalThis.editorControllers[tab.id]?.downloadAttachment(attachment);
return true;
},
onPreviewAttachment(editor, attachment) {
previewAttachment(attachment) {
globalThis.editorControllers[tab.id]?.previewAttachment(attachment);
return true;
},
@@ -99,6 +99,23 @@ const Tiptap = ({
attachment
) as Promise<string | undefined>;
},
createInternalLink(attributes) {
logger("info", "create internal link");
return new Promise((resolve) => {
const id = randId("createInternalLink");
globalThis.pendingResolvers[id] = (value) => {
delete globalThis.pendingResolvers[id];
resolve(value);
logger("info", "resolved create link request:", id);
};
post("editor-events:create-internal-link", {
attributes: attributes,
resolverId: id
});
});
},
element: getContentDiv(),
editable: !settings.readonly,
editorProps: {
@@ -107,7 +124,7 @@ const Tiptap = ({
content: globalThis.editorControllers[tab.id]?.content?.current,
isMobile: true,
doubleSpacedLines: settings.doubleSpacedLines,
onOpenLink: (url) => {
openLink: (url) => {
return globalThis.editorControllers[tab.id]?.openLink(url) || true;
},
copyToClipboard: (text) => {

View File

@@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
ControlledMenu,
// applyStatics
MenuItem as MenuItemInner,
SubMenu as SubMenuInner,
MenuDivider
} from "@szhsin/react-menu";
import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
@@ -32,12 +26,13 @@ import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import DotsVerticalIcon from "mdi-react/DotsVerticalIcon";
import FullscreenIcon from "mdi-react/FullscreenIcon";
import MagnifyIcon from "mdi-react/MagnifyIcon";
import TableOfContentsIcon from "mdi-react/TableOfContentsIcon";
import React, { useRef, useState } from "react";
import { useSafeArea } from "../hooks/useSafeArea";
import { useTabContext, useTabStore } from "../hooks/useTabStore";
import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css";
import TableOfContentsIcon from "mdi-react/TableOfContentsIcon";
const menuClassName = ({ state }: any) =>
state === "opening"
? styles.menuOpening
@@ -52,22 +47,10 @@ const menuItemClassName = ({ hover, disabled }: any) =>
? styles.menuItemHover
: styles.menuItem;
const submenuItemClassName = (modifiers: any) =>
`${styles.submenuItem} ${menuItemClassName(modifiers)}`;
const MenuItem = (props: any) => (
<MenuItemInner {...props} className={menuItemClassName} />
);
const SubMenu = (props: any) => (
<SubMenuInner
{...props}
menuClassName={menuClassName}
itemProps={{ className: submenuItemClassName }}
offsetY={-7}
/>
);
const Button = ({
onPress,
children,
@@ -178,65 +161,6 @@ function Header({
flexDirection: "row"
}}
>
<Button
onPress={() => {
editor?.commands.undo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_background)"
: "var(--nn_primary_paragraph)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_background)"
: "var(--nn_primary_paragraph)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
{!settings.premium && (
<Button
onPress={() => {
@@ -266,32 +190,6 @@ function Header({
</Button>
)}
<Button
onPress={() => {
editor?.commands.startSearch();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<MagnifyIcon
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_paragraph)"
/>
</Button>
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
<Button
onPress={() => {
@@ -342,8 +240,9 @@ function Header({
<div
style={{
border: "2.5px solid var(--nn_primary_icon)",
width: 20 * settings.fontScale,
height: 20 * settings.fontScale,
width: 19 * settings.fontScale,
height: 19 * settings.fontScale,
minWidth: 19 * settings.fontScale,
borderRadius: 5,
display: "flex",
justifyContent: "center",
@@ -417,6 +316,102 @@ function Header({
}
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
width: "100%"
}}
>
<Button
onPress={() => {
editor?.commands.undo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_paragraph)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_paragraph)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.startSearch();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<MagnifyIcon
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_paragraph)"
/>
</Button>
</div>
<MenuItem
value="toc"
style={{

View File

@@ -271,8 +271,8 @@ export function useEditorController({
case "native:status":
break;
case "native:keyboardShown":
if (editor?.current) {
scrollIntoView(editor?.current as any);
if (editor) {
scrollIntoView(editor as any);
}
break;
case "native:attachment-data":

View File

@@ -23,6 +23,7 @@ import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { EditorController } from "../hooks/useEditorController";
globalThis.sessionId = "notesnook-editor";
globalThis.pendingResolvers = {};
globalThis.pendingResolvers = {};
export function randId(prefix: string) {
@@ -80,6 +81,7 @@ declare global {
var noToolbar: boolean;
var noHeader: boolean;
function toBlobURL(dataurl: string, id?: string): string | undefined;
var pendingResolvers: { [name: string]: (value: any) => 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;
}

View File

@@ -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",