mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
/*
|
|
This file is part of the Notesnook project (https://notesnook.com/)
|
|
|
|
Copyright (C) 2022 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 React, { createRef, useEffect, useState } from "react";
|
|
import { Keyboard, TouchableOpacity, View } from "react-native";
|
|
import { FlatList } from "react-native-gesture-handler";
|
|
import { notesnook } from "../../../../e2e/test.ids";
|
|
import { db } from "../../../common/database";
|
|
import {
|
|
eSubscribeEvent,
|
|
eUnSubscribeEvent,
|
|
ToastEvent
|
|
} from "../../../services/event-manager";
|
|
import Navigation from "../../../services/navigation";
|
|
import SearchService from "../../../services/search";
|
|
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
|
import { useSelectionStore } from "../../../stores/use-selection-store";
|
|
import { useThemeStore } from "../../../stores/use-theme-store";
|
|
import { getTotalNotes } from "../../../utils";
|
|
import { eOpenMoveNoteDialog } from "../../../utils/events";
|
|
import layoutmanager from "../../../utils/layout-manager";
|
|
import { SIZE } from "../../../utils/size";
|
|
import { Dialog } from "../../dialog";
|
|
import DialogHeader from "../../dialog/dialog-header";
|
|
import { presentDialog } from "../../dialog/functions";
|
|
import { Button } from "../../ui/button";
|
|
import { IconButton } from "../../ui/icon-button";
|
|
import Input from "../../ui/input";
|
|
import { PressableButton } from "../../ui/pressable";
|
|
import SheetWrapper from "../../ui/sheet";
|
|
import Heading from "../../ui/typography/heading";
|
|
import Paragraph from "../../ui/typography/paragraph";
|
|
import { useCallback } from "react";
|
|
let newNotebookTitle = null;
|
|
const notebookInput = createRef();
|
|
const actionSheetRef = createRef();
|
|
const AddToNotebookSheet = () => {
|
|
const [visible, setVisible] = useState(false);
|
|
const [note, setNote] = useState(null);
|
|
|
|
function open(note) {
|
|
setNote(note);
|
|
setVisible(true);
|
|
actionSheetRef.current?.setModalVisible(true);
|
|
}
|
|
|
|
useEffect(() => {
|
|
eSubscribeEvent(eOpenMoveNoteDialog, open);
|
|
return () => {
|
|
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
|
|
};
|
|
}, []);
|
|
|
|
const _onClose = () => {
|
|
setVisible(false);
|
|
newNotebookTitle = null;
|
|
setNote(null);
|
|
Navigation.queueRoutesForUpdate(
|
|
"Notes",
|
|
"Favorites",
|
|
"ColoredNotes",
|
|
"TaggedNotes",
|
|
"TopicNotes",
|
|
"Notebooks",
|
|
"Notebook"
|
|
);
|
|
};
|
|
|
|
return !visible ? null : (
|
|
<SheetWrapper fwdRef={actionSheetRef} onClose={_onClose}>
|
|
<MoveNoteComponent note={note} />
|
|
</SheetWrapper>
|
|
);
|
|
};
|
|
|
|
export default AddToNotebookSheet;
|
|
|
|
const MoveNoteComponent = ({ note }) => {
|
|
const colors = useThemeStore((state) => state.colors);
|
|
|
|
const notebooks = useNotebookStore((state) =>
|
|
state.notebooks.filter((n) => n?.type === "notebook")
|
|
);
|
|
|
|
const selectedItemsList = useSelectionStore(
|
|
(state) => state.selectedItemsList
|
|
);
|
|
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
|
const [expanded, setExpanded] = useState("");
|
|
const [notebookInputFocused, setNotebookInputFocused] = useState(false);
|
|
const [noteExists, setNoteExists] = useState([]);
|
|
const addNewNotebook = async () => {
|
|
if (!newNotebookTitle || newNotebookTitle.trim().length === 0)
|
|
return ToastEvent.show({
|
|
heading: "Notebook title is required",
|
|
type: "error",
|
|
context: "local"
|
|
});
|
|
|
|
let id = await db.notebooks.add({
|
|
title: newNotebookTitle,
|
|
description: null,
|
|
topics: [],
|
|
id: null
|
|
});
|
|
|
|
setExpanded(id);
|
|
openAddTopicDialog(db.notebooks.notebook(id).data);
|
|
notebookInput.current?.clear();
|
|
notebookInput.current?.blur();
|
|
setNotebooks();
|
|
updateNoteExists();
|
|
};
|
|
|
|
const addNewTopic = async (value, item) => {
|
|
if (!value || value.trim().length === 0) {
|
|
ToastEvent.show({
|
|
heading: "Topic title is required",
|
|
type: "error",
|
|
context: "local"
|
|
});
|
|
return false;
|
|
}
|
|
|
|
await db.notebooks.notebook(item.id).topics.add(value);
|
|
setNotebooks();
|
|
updateNoteExists();
|
|
return true;
|
|
};
|
|
|
|
const handlePress = async (item) => {
|
|
let noteIds =
|
|
selectedItemsList.length > 0
|
|
? selectedItemsList.map((n) => n.id)
|
|
: [note?.id];
|
|
|
|
if (getCount(item)) {
|
|
await db.notebooks
|
|
.notebook(item.notebookId)
|
|
.topics.topic(item.id)
|
|
.delete(...noteIds);
|
|
} else {
|
|
await db.notes.move(
|
|
{
|
|
topic: item.id,
|
|
id: item.notebookId
|
|
},
|
|
...noteIds
|
|
);
|
|
}
|
|
|
|
Navigation.queueRoutesForUpdate(
|
|
"Notes",
|
|
"Favorites",
|
|
"ColoredNotes",
|
|
"TaggedNotes",
|
|
"TopicNotes"
|
|
);
|
|
|
|
setNotebooks();
|
|
updateNoteExists();
|
|
SearchService.updateAndSearch();
|
|
};
|
|
|
|
useEffect(() => {
|
|
updateNoteExists();
|
|
}, [updateNoteExists]);
|
|
|
|
const updateNoteExists = useCallback(() => {
|
|
if (!note?.id && selectedItemsList?.length === 0) return;
|
|
|
|
let notes =
|
|
selectedItemsList.length > 0
|
|
? selectedItemsList.map((n) => n.id)
|
|
: [note?.id];
|
|
let ids = [];
|
|
let notebooks = db.notebooks.all;
|
|
for (let i = 0; i < notebooks.length; i++) {
|
|
if (notebooks[i].topics) {
|
|
for (let t = 0; t < notebooks[i].topics.length; t++) {
|
|
let topic = notebooks[i].topics[t];
|
|
if (topic.type !== "topic") continue;
|
|
for (let id of notes) {
|
|
if (topic.notes.indexOf(id) > -1) {
|
|
if (ids.indexOf(notebooks[i].id) === -1) {
|
|
ids.push(notebooks[i].id);
|
|
}
|
|
if (ids.indexOf(topic.id) === -1) ids.push(topic.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setNoteExists(ids);
|
|
}, [note?.id, selectedItemsList]);
|
|
|
|
const openAddTopicDialog = (item) => {
|
|
presentDialog({
|
|
context: "move_note",
|
|
input: true,
|
|
inputPlaceholder: "Enter title",
|
|
title: "New topic",
|
|
paragraph: "Add a new topic in " + item.title,
|
|
positiveText: "Add",
|
|
positivePress: (value) => {
|
|
return addNewTopic(value, item);
|
|
}
|
|
});
|
|
};
|
|
|
|
const getCount = (topic) => {
|
|
if (!topic) return;
|
|
let notes =
|
|
selectedItemsList.length > 0
|
|
? selectedItemsList.map((n) => n.id)
|
|
: [note?.id];
|
|
let count = 0;
|
|
for (let id of notes) {
|
|
if (topic.notes.indexOf(id) > -1) {
|
|
count++;
|
|
}
|
|
}
|
|
return count > 0 && notes.length > 0
|
|
? `${count} of ${notes.length} selected notes exist in this topic. (Tap to remove)`
|
|
: null;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Dialog context="move_note" />
|
|
<View>
|
|
<TouchableOpacity
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
position: "absolute"
|
|
}}
|
|
onPress={() => {
|
|
Keyboard.dismiss();
|
|
}}
|
|
/>
|
|
<View
|
|
style={{
|
|
paddingHorizontal: 12,
|
|
flexDirection: "row",
|
|
justifyContent: "space-between"
|
|
}}
|
|
>
|
|
<DialogHeader
|
|
title="Add to notebook"
|
|
paragraph={"Add your notes to notebooks to find them easily."}
|
|
/>
|
|
</View>
|
|
|
|
<FlatList
|
|
nestedScrollEnabled={true}
|
|
onMomentumScrollEnd={() => {
|
|
actionSheetRef.current?.handleChildScrollEnd();
|
|
}}
|
|
keyboardShouldPersistTaps="always"
|
|
keyboardDismissMode="none"
|
|
data={notebooks}
|
|
ListFooterComponent={
|
|
<View
|
|
style={{
|
|
height: 200
|
|
}}
|
|
/>
|
|
}
|
|
ListHeaderComponent={
|
|
<View
|
|
style={{
|
|
width: "100%",
|
|
marginTop: 10
|
|
}}
|
|
>
|
|
<Input
|
|
fwdRef={notebookInput}
|
|
onChangeText={(value) => {
|
|
newNotebookTitle = value;
|
|
}}
|
|
testID={notesnook.ids.dialogs.addTo.addNotebook}
|
|
blurOnSubmit={false}
|
|
onFocusInput={() => {
|
|
setNotebookInputFocused(true);
|
|
}}
|
|
onBlurInput={() => {
|
|
setNotebookInputFocused(false);
|
|
}}
|
|
button={{
|
|
icon: "check",
|
|
color: notebookInputFocused ? colors.accent : colors.icon,
|
|
onPress: addNewNotebook
|
|
}}
|
|
onSubmit={addNewNotebook}
|
|
placeholder="Create a new notebook"
|
|
/>
|
|
</View>
|
|
}
|
|
style={{
|
|
paddingHorizontal: 12
|
|
}}
|
|
renderItem={({ item }) => (
|
|
<View
|
|
style={{
|
|
borderWidth: 1,
|
|
borderColor: expanded ? colors.nav : "transparent",
|
|
borderRadius: 6,
|
|
overflow: "hidden",
|
|
marginBottom: 10
|
|
}}
|
|
>
|
|
<PressableButton
|
|
onPress={() => {
|
|
if (!item.topics || item.topics.length === 0) {
|
|
setExpanded(item.id);
|
|
openAddTopicDialog(item);
|
|
return;
|
|
}
|
|
layoutmanager.withAnimation(200);
|
|
setExpanded(item.id === expanded ? null : item.id);
|
|
setNotebookInputFocused(false);
|
|
}}
|
|
type="grayBg"
|
|
customStyle={{
|
|
height: 50,
|
|
width: "100%",
|
|
borderRadius: 5,
|
|
alignItems: "flex-start"
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
width: "100%",
|
|
height: 50,
|
|
justifyContent: "space-between",
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: 12
|
|
}}
|
|
>
|
|
<View>
|
|
<Heading
|
|
color={
|
|
noteExists.indexOf(item.id) > -1 ? colors.accent : null
|
|
}
|
|
size={SIZE.md}
|
|
>
|
|
{item.title}
|
|
</Heading>
|
|
{item.topics?.length > 0 ? (
|
|
<Paragraph size={SIZE.xs} color={colors.icon}>
|
|
{getTotalNotes(item) + " notes" + " & "}
|
|
{item.topics.length === 1
|
|
? item.topics.length + " topic"
|
|
: item.topics.length + " topics"}
|
|
</Paragraph>
|
|
) : null}
|
|
</View>
|
|
|
|
<IconButton
|
|
name={expanded === item.id ? "plus" : "chevron-down"}
|
|
color={expanded === item.id ? colors.accent : colors.pri}
|
|
size={SIZE.xl}
|
|
onPress={() => {
|
|
if (expanded !== item.id) {
|
|
setExpanded(item.id);
|
|
return;
|
|
}
|
|
layoutmanager.withAnimation(200);
|
|
setExpanded(item.id);
|
|
openAddTopicDialog(item);
|
|
}}
|
|
/>
|
|
</View>
|
|
</PressableButton>
|
|
|
|
{expanded === item.id ? (
|
|
<FlatList
|
|
nestedScrollEnabled
|
|
data={item.topics?.filter((t) => t.type === "topic")}
|
|
keyboardShouldPersistTaps="always"
|
|
keyboardDismissMode="none"
|
|
onMomentumScrollEnd={() => {
|
|
actionSheetRef.current?.handleChildScrollEnd();
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
alignSelf: "flex-end",
|
|
maxHeight: 500
|
|
}}
|
|
renderItem={({ item, index }) => (
|
|
<PressableButton
|
|
onPress={() => handlePress(item, index)}
|
|
type="gray"
|
|
customStyle={{
|
|
minHeight: 50,
|
|
borderTopWidth: index === 0 ? 0 : 1,
|
|
borderTopColor: index === 0 ? null : colors.nav,
|
|
width: "100%",
|
|
borderRadius: 0,
|
|
alignItems: "center",
|
|
flexDirection: "row",
|
|
paddingHorizontal: 12,
|
|
justifyContent: "space-between",
|
|
paddingVertical: 12
|
|
}}
|
|
>
|
|
<View>
|
|
<Paragraph color={colors.heading}>
|
|
{item.title}
|
|
</Paragraph>
|
|
<Paragraph color={colors.icon} size={SIZE.xs}>
|
|
{item.notes.length + " notes"}
|
|
</Paragraph>
|
|
{getCount(item) ? (
|
|
<View
|
|
style={{
|
|
backgroundColor: colors.nav,
|
|
borderRadius: 5,
|
|
paddingHorizontal: 5,
|
|
paddingVertical: 2,
|
|
marginTop: 5
|
|
}}
|
|
>
|
|
<Paragraph color={colors.icon} size={SIZE.xs}>
|
|
{getCount(item)}
|
|
</Paragraph>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
{noteExists.indexOf(item.id) > -1 ? (
|
|
<Button
|
|
onPress={() => handlePress(item, index)}
|
|
icon="check"
|
|
testID="icon-check"
|
|
iconColor={colors.accent}
|
|
iconSize={SIZE.lg}
|
|
height={35}
|
|
width={35}
|
|
style={{
|
|
borderRadius: 100,
|
|
paddingHorizontal: 0
|
|
}}
|
|
/>
|
|
) : null}
|
|
</PressableButton>
|
|
)}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
)}
|
|
/>
|
|
</View>
|
|
</>
|
|
);
|
|
};
|