Files
notesnook/apps/mobile/app/components/sheets/topic-sheet/index.tsx

549 lines
15 KiB
TypeScript
Raw Normal View History

2023-03-16 21:22:21 +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 qclone from "qclone";
import React, {
createContext,
useCallback,
2023-03-16 21:22:21 +05:00
useContext,
useEffect,
useRef,
useState,
RefObject
2023-03-16 21:22:21 +05:00
} from "react";
2023-03-31 01:40:34 +05:00
import { Platform, RefreshControl, View } from "react-native";
2023-03-16 21:22:21 +05:00
import ActionSheet, {
ActionSheetRef,
FlatList
} from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { IconButton } from "../../../components/ui/icon-button";
import { PressableButton } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
2023-03-16 21:22:21 +05:00
} from "../../../services/event-manager";
import useNavigationStore, {
NotebookScreenParams
} from "../../../stores/use-navigation-store";
import { useThemeStore } from "../../../stores/use-theme-store";
2023-03-20 11:28:46 +05:00
import {
eOnNewTopicAdded,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
2023-03-16 21:22:21 +05:00
import { normalize, SIZE } from "../../../utils/size";
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
2023-03-16 21:22:21 +05:00
2023-03-31 00:55:12 +05:00
import { groupArray } from "@notesnook/core/utils/grouping";
import Config from "react-native-config";
2023-03-16 21:22:21 +05:00
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
2023-03-31 00:55:12 +05:00
import { notesnook } from "../../../../e2e/test.ids";
2023-03-16 21:22:21 +05:00
import { openEditor } from "../../../screens/notes/common";
import { getTotalNotes, history } from "../../../utils";
import { deleteItems } from "../../../utils/functions";
import { presentDialog } from "../../dialog/functions";
2023-03-31 00:55:12 +05:00
import { Properties } from "../../properties";
import Sort from "../sort";
import { MMKV } from "../../../common/database/mmkv";
class TopicSheetConfig {
static storageKey: "$$sp";
static makeId(item: any) {
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
}
static get(item: any) {
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
}
static set(item: any, index = 0) {
MMKV.setInt(TopicSheetConfig.makeId(item), index);
}
}
2023-03-17 16:18:57 +05:00
2023-03-16 21:22:21 +05:00
export const TopicsSheet = () => {
const [collapsed, setCollapsed] = useState(false);
2023-03-16 21:22:21 +05:00
const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow =
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
const [notebook, setNotebook] = useState(
canShow
? db.notebooks?.notebook(
currentScreen?.notebookId || currentScreen?.id || ""
)?.data
: null
);
const [selection, setSelection] = useState<TopicType[]>([]);
const [enabled, setEnabled] = useState(false);
const colors = useThemeStore((state) => state.colors);
const ref = useRef<ActionSheetRef>(null);
const isTopic = currentScreen.name === "TopicNotes";
const [topics, setTopics] = useState(
notebook
? qclone(
groupArray(notebook.topics, db.settings?.getGroupOptions("topics"))
)
: []
);
2023-03-31 01:39:41 +05:00
const [groupOptions, setGroupOptions] = useState(
db.settings?.getGroupOptions("topics")
);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
}, []);
useEffect(() => {
eSubscribeEvent("groupOptionsUpdate", onUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
};
}, [onUpdate]);
2023-03-16 21:22:21 +05:00
const onRequestUpdate = React.useCallback(
(data?: NotebookScreenParams) => {
if (!canShow) return;
if (!data) data = { item: notebook } as NotebookScreenParams;
const _notebook = db.notebooks?.notebook(data.item?.id)
?.data as NotebookType;
if (_notebook) {
setNotebook(_notebook);
setTopics(
qclone(
groupArray(_notebook.topics, db.settings?.getGroupOptions("topics"))
)
);
2023-03-16 21:22:21 +05:00
}
},
[canShow, notebook]
2023-03-16 21:22:21 +05:00
);
useEffect(() => {
2023-03-20 11:28:46 +05:00
const onTopicUpdate = () => {
onRequestUpdate();
};
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
2023-03-16 21:22:21 +05:00
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
return () => {
2023-03-20 11:28:46 +05:00
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
2023-03-16 21:22:21 +05:00
};
}, [onRequestUpdate]);
const PLACEHOLDER_DATA = {
heading: "Topics",
paragraph: "You have not added any topics yet.",
button: "Add first topic",
action: () => {
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
},
loading: "Loading notebook topics"
};
const renderTopic = ({
item,
index
}: {
item: TopicType | GroupHeader;
index: number;
}) =>
(item as GroupHeader).type === "header" ? null : (
<TopicItem sheetRef={ref} item={item as TopicType} index={index} />
);
2023-03-16 21:22:21 +05:00
const selectionContext = {
selection: selection,
enabled,
setEnabled,
toggleSelection: (item: TopicType) => {
setSelection((state) => {
const selection = [...state];
const index = selection.findIndex(
(selected) => selected.id === item.id
);
if (index > -1) {
selection.splice(index, 1);
if (selection.length === 0) {
setEnabled(false);
}
return selection;
}
selection.push(item);
return selection;
});
}
};
useEffect(() => {
if (canShow) {
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
const notebook = db.notebooks?.notebook(id as string)?.data;
ref.current?.show(
TopicSheetConfig.get({
type: isTopic ? "topic" : "notebook",
id: currentScreen.id
})
);
2023-03-31 00:55:12 +05:00
setTimeout(() => {
if (notebook) {
2023-03-31 01:39:41 +05:00
onRequestUpdate({
item: notebook
2023-03-31 01:39:41 +05:00
} as any);
}
}, 1);
2023-03-16 21:22:21 +05:00
} else {
ref.current?.hide();
}
}, [
canShow,
currentScreen?.id,
currentScreen.name,
currentScreen?.notebookId,
onRequestUpdate,
isTopic
2023-03-16 21:22:21 +05:00
]);
return (
<ActionSheet
ref={ref}
isModal={false}
containerStyle={{
2023-03-31 00:55:12 +05:00
maxHeight: 300,
2023-03-16 21:22:21 +05:00
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: colors.bg,
borderWidth: 1,
2023-03-31 01:39:41 +05:00
borderColor: colors.nav,
2023-03-16 21:22:21 +05:00
borderBottomWidth: 0
}}
2023-03-31 01:39:41 +05:00
openAnimationConfig={{
friction: 10
}}
onSnapIndexChange={(index) => {
setCollapsed(index === 0);
TopicSheetConfig.set(
{
type: isTopic ? "topic" : "notebook",
id: currentScreen.id
},
index
);
}}
2023-03-16 21:22:21 +05:00
closable={!canShow}
elevation={10}
indicatorStyle={{
width: 100,
backgroundColor: colors.nav
}}
keyboardHandlerEnabled={false}
snapPoints={
Config.isTesting === "true"
? [100]
: [Platform.OS === "ios" ? 25 : 20, 100]
}
initialSnapIndex={1}
2023-03-16 21:22:21 +05:00
backgroundInteractionEnabled
gestureEnabled
2023-03-31 01:39:41 +05:00
>
<View
style={{
position: "absolute",
right: 12,
marginTop: -80
}}
>
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={"accent"}
accentText="light"
onPress={openEditor}
customStyle={{
borderRadius: 100
2023-03-16 21:22:21 +05:00
}}
>
2023-03-31 01:39:41 +05:00
<View
style={{
alignItems: "center",
justifyContent: "center",
height: normalize(60),
width: normalize(60)
2023-03-16 21:22:21 +05:00
}}
>
2023-03-31 01:39:41 +05:00
<Icon name="plus" color="white" size={SIZE.xxl} />
</View>
</PressableButton>
</View>
2023-03-16 21:22:21 +05:00
<View
style={{
2023-03-31 00:55:12 +05:00
maxHeight: 300,
height: 300,
2023-03-16 21:22:21 +05:00
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
alignItems: "center"
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
TOPICS
</Paragraph>
<View
style={{
flexDirection: "row"
}}
>
{enabled ? (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={async () => {
//@ts-ignore
history.selectedItemsList = selection;
presentDialog({
title: `Delete ${
selection.length > 1 ? "topics" : "topics"
}`,
paragraph: `Are you sure you want to delete ${
selection.length > 1 ? "these topicss?" : "this topics?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
await deleteItems();
history.selectedItemsList = [];
setEnabled(false);
setSelection([]);
},
positiveType: "errorShade"
});
return;
}}
color={colors.pri}
tooltipText="Move to trash"
tooltipPosition={1}
name="delete"
size={22}
/>
) : (
<>
<IconButton
name={
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
onPress={() => {
presentSheet({
component: <Sort screen="TopicSheet" type="topics" />
});
}}
testID="group-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name="plus"
onPress={PLACEHOLDER_DATA.action}
testID="add-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name={collapsed ? "chevron-up" : "chevron-down"}
onPress={() => {
if (ref.current?.currentSnapIndex() !== 0) {
setCollapsed(true);
ref.current?.snapToIndex(0);
} else {
setCollapsed(false);
ref.current?.snapToIndex(1);
}
}}
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
</>
2023-03-16 21:22:21 +05:00
)}
</View>
</View>
<SelectionContext.Provider value={selectionContext}>
<FlatList
data={topics}
style={{
width: "100%"
}}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
onRequestUpdate();
}}
2023-03-17 14:26:48 +05:00
colors={[colors.accent]}
progressBackgroundColor={colors.bg}
2023-03-16 21:22:21 +05:00
/>
}
keyExtractor={(item) => (item as TopicType).id}
2023-03-16 21:22:21 +05:00
renderItem={renderTopic}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.icon}>No topics</Paragraph>
</View>
}
2023-03-16 21:22:21 +05:00
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</SelectionContext.Provider>
</View>
</ActionSheet>
);
};
const SelectionContext = createContext<{
selection: TopicType[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: TopicType) => void;
}>({
selection: [],
enabled: false,
setEnabled: (value: boolean) => {},
toggleSelection: (item: TopicType) => {}
});
const useSelection = () => useContext(SelectionContext);
const TopicItem = ({
item,
index,
sheetRef
}: {
item: TopicType;
index: number;
sheetRef: RefObject<ActionSheetRef>;
}) => {
2023-03-16 21:22:21 +05:00
const screen = useNavigationStore((state) => state.currentScreen);
const colors = useThemeStore((state) => state.colors);
const selection = useSelection();
const isSelected =
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
const isFocused = screen.id === item.id;
const notesCount = getTotalNotes(item);
return (
<PressableButton
type={isSelected || isFocused ? "grayBg" : "transparent"}
onLongPress={() => {
if (selection.enabled) return;
selection.setEnabled(true);
selection.toggleSelection(item);
}}
2023-03-17 14:39:17 +05:00
testID={`topic-sheet-item-${index}`}
2023-03-16 21:22:21 +05:00
onPress={() => {
if (selection.enabled) {
selection.toggleSelection(item);
return;
}
TopicNotes.navigate(item, true);
sheetRef.current?.snapToIndex(0);
2023-03-16 21:22:21 +05:00
}}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 12,
borderRadius: 0
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{selection.enabled ? (
<IconButton
size={SIZE.lg}
color={isSelected ? colors.accent : colors.icon}
name={
isSelected
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
) : null}
<Paragraph size={SIZE.sm}>
{item.title}{" "}
{notesCount ? (
<Paragraph size={SIZE.xs} color={colors.icon}>
{notesCount}
</Paragraph>
) : null}
</Paragraph>
</View>
<IconButton
name="dots-horizontal"
customStyle={{
width: 40,
height: 40
}}
2023-03-17 16:18:57 +05:00
testID={notesnook.ids.notebook.menu}
2023-03-16 21:22:21 +05:00
onPress={() => {
Properties.present(item);
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.pri}
size={SIZE.xl}
/>
</PressableButton>
);
};