Files
notesnook/apps/mobile/app/components/sheets/references/index.tsx

522 lines
13 KiB
TypeScript
Raw Normal View History

2024-01-24 18:58:14 +05:00
/*
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: {}
});
}
});
};