mobile: push changes

This commit is contained in:
Ammar Ahmed
2023-11-22 11:16:15 +05:00
committed by Abdullah Atta
parent 1a61c4ee8f
commit ceb6e94d0c
57 changed files with 1636 additions and 2200 deletions

View File

@@ -108,7 +108,7 @@ class RNSqliteConnection implements DatabaseConnection {
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
console.log("SQLITE result:", result?.rows?._array);
// console.log("SQLITE result:", result?.rows?._array);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []

View File

@@ -22,7 +22,6 @@ import { KeyboardAvoidingView, Platform } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header";
export const Container = ({ children }: PropsWithChildren) => {
@@ -46,7 +45,6 @@ export const Container = ({ children }: PropsWithChildren) => {
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}

View File

@@ -20,39 +20,70 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { SearchBar } from "../../screens/search/search-bar";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import useNavigationStore, {
RouteName
} from "../../stores/use-navigation-store";
const _Header = () => {
type HeaderRightButton = {
title: string;
onPress: () => void;
};
export const Header = ({
renderedInRoute,
onLeftMenuButtonPress,
title,
titleHiddenOnRender,
headerRightButtons,
id,
accentColor,
isBeta,
canGoBack,
onPressDefaultRightButton,
hasSearch,
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];
titleHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [hide, setHide] = useState(true);
const [borderHidden, setBorderHidden] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const currentScreen = useNavigationStore(
(state) => state.currentScreen?.name
);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const onScroll = useCallback(
(data: { x: number; y: number }) => {
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
setHide(false);
if (!borderHidden) return;
setBorderHidden(false);
} else {
if (hide) return;
setHide(true);
if (borderHidden) return;
setBorderHidden(true);
}
},
[hide]
[borderHidden, id, renderedInRoute]
);
useEffect(() => {
@@ -60,9 +91,9 @@ const _Header = () => {
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [borderHidden, onScroll]);
return selectionMode ? null : (
return selectionMode && isFocused ? null : (
<>
<View
style={[
@@ -72,29 +103,42 @@ const _Header = () => {
backgroundColor: colors.primary.background,
overflow: "hidden",
borderBottomWidth: 1,
borderBottomColor: hide
borderBottomColor: borderHidden
? "transparent"
: colors.secondary.background,
justifyContent: "space-between"
}
]}
>
{currentScreen === "Search" ? (
<SearchBar />
) : (
<>
<View style={styles.leftBtnContainer}>
<LeftMenus />
<Title />
</View>
<RightMenus />
</>
)}
<>
<View style={styles.leftBtnContainer}>
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
<Title
isHiddenOnRender={titleHiddenOnRender}
renderedInRoute={renderedInRoute}
id={id}
accentColor={accentColor}
title={title}
isBeta={isBeta}
/>
</View>
<RightMenus
renderedInRoute={renderedInRoute}
id={id}
headerRightButtons={headerRightButtons}
onPressDefaultRightButton={onPressDefaultRightButton}
search={hasSearch}
onSearch={onSearch}
/>
</>
</View>
</>
);
};
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({
container: {

View File

@@ -17,23 +17,29 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import { DDS } from "../../services/device-detection";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { tabBarRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = () => {
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet";
const onLeftButtonPress = () => {
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
@@ -60,7 +66,7 @@ export const LeftMenus = () => {
left={40}
top={40}
right={DDS.isLargeTablet() ? 10 : 10}
onPress={onLeftButtonPress}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}

View File

@@ -20,40 +20,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import {
HeaderRightButton,
RouteName
} from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
export const RightMenus = () => {
export const RightMenus = ({
headerRightButtons,
renderedInRoute,
id,
onPressDefaultRightButton,
search,
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode);
const buttons = useNavigationStore((state) => state.headerRightButtons);
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
const buttonAction = useNavigationStore((state) => state.buttonAction);
const menuRef = useRef<Menu>(null);
return (
<View style={styles.rightBtnContainer}>
{!currentScreen.startsWith("Settings") ? (
{search ? (
<IconButton
onPress={async () => {
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
onPress={onSearch}
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
@@ -63,9 +69,9 @@ export const RightMenus = () => {
{deviceMode !== "mobile" ? (
<Button
onPress={buttonAction}
onPress={onPressDefaultRightButton}
testID={notesnook.ids.default.addBtn}
icon={currentScreen === "Trash" ? "delete" : "plus"}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="shade"
hitSlop={{
@@ -86,7 +92,7 @@ export const RightMenus = () => {
/>
) : null}
{buttons && buttons.length > 0 ? (
{headerRightButtons && headerRightButtons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -108,7 +114,7 @@ export const RightMenus = () => {
/>
}
>
{buttons.map((item) => (
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,

View File

@@ -20,105 +20,74 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Tag from "../ui/tag";
import Heading from "../ui/typography/heading";
const titleState: { [id: string]: boolean } = {};
export const Title = () => {
export const Title = ({
title,
isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();
const currentScreen = useNavigationStore((state) => state.currentScreen);
const isNotebook = currentScreen.name === "Notebook";
const isTopic = currentScreen?.name === "TopicNotes";
const [hide, setHide] = useState(
isNotebook
? typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true
: false
);
const isHidden = titleState[currentScreen.id as string];
const notebook =
isTopic && currentScreen.notebookId
? db.notebooks?.notebook(currentScreen.notebookId)?.data
: null;
const title = currentScreen.title;
const isTag = currentScreen?.name === "TaggedNotes";
const [visible, setVisible] = useState(isHiddenOnRender);
const isTag = title.startsWith("#");
const onScroll = useCallback(
(data: { x: number; y: number }) => {
if (currentScreen.name !== "Notebook") {
setHide(false);
return;
}
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
titleState[currentScreen.id as string] = false;
setHide(false);
if (!visible) return;
setVisible(false);
} else {
if (hide) return;
titleState[currentScreen.id as string] = true;
setHide(true);
if (visible) return;
setVisible(true);
}
},
[currentScreen.id, currentScreen.name, hide]
[id, renderedInRoute, visible]
);
useEffect(() => {
if (currentScreen.name === "Notebook") {
const value =
typeof titleState[currentScreen.id] === "boolean"
? titleState[currentScreen.id]
: true;
setHide(value);
} else {
setHide(titleState[currentScreen.id as string]);
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [visible, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
if (notebook) {
NotebookScreen.navigate(notebook, true);
}
}
return (
<>
{!hide && !isHidden ? (
{!visible ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.lg}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={currentScreen.color || colors.primary.heading}
color={accentColor || colors.primary.heading}
>
{isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{title}{" "}
{isTag ? title.slice(1) : title}{" "}
<Tag
visible={currentScreen.beta}
visible={isBeta}
text="BETA"
style={{
backgroundColor: "transparent"

View File

@@ -120,6 +120,7 @@ export const SectionHeader = React.memo<
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={dataType} />
});
@@ -169,7 +170,7 @@ export const SectionHeader = React.memo<
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: isCompactModeEnabled
: "notebooksListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});

View File

@@ -105,7 +105,7 @@ const NoteItem = ({
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().currentScreen?.id
item.id !== useNavigationStore.getState().currentRoute?.id
)
.map((item) => (
<Button

View File

@@ -49,98 +49,96 @@ type EmptyListProps = {
screen?: string;
};
export const Empty = React.memo(
function Empty({
loading = true,
placeholder,
title,
color,
dataType,
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
export const Empty = React.memo(function Empty({
loading = true,
placeholder,
title,
color,
dataType,
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={color ? color : "accent"}
tip={tip || ({ text: placeholder?.paragraph } as TTip)}
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: color || colors.primary.accent
}}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: color || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={color || colors.primary.accent}
/>
</View>
</>
)}
</View>
);
},
(prev, next) => {
if (prev.loading === next.loading) return true;
return false;
}
);
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={color || colors.primary.accent}
/>
</View>
</>
)}
</View>
);
});
/**
* Make a tips manager.

View File

@@ -201,6 +201,7 @@ export default function List(props: ListProps) {
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
/>
) : null
}

View File

@@ -251,7 +251,7 @@ async function resolveNotes(ids: string[]) {
group.notebooks.map((id) => resolved.notebooks[id])
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all"))?.length || 0
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
};
}
return data;

View File

@@ -17,39 +17,28 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeColors } from "@notesnook/theme";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eClearEditor } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
async function getNotebooks(item) {
let filteredNotebooks = [];
const relations = await db.relations.to(note, "notebook").resolve();
filteredNotebooks.push(relations);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
return filteredNotebooks;
}
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
@@ -60,11 +49,6 @@ export default function Notebooks({ note, close, full }) {
NotebookScreen.navigate(item, true);
};
const navigateTopic = (id, notebookId) => {
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<View
key={item.id}
@@ -105,56 +89,6 @@ export default function Notebooks({ note, close, full }) {
>
{item.title}
</Heading>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.primary.hover,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
await db.notes.removeFromNotebook(
{
id: item.id,
topic: topic.id
},
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: "Note removed from topic",
context: "local",
type: "success"
});
}}
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</View>
);

View File

@@ -46,8 +46,7 @@ export const SelectionHeader = React.memo(() => {
);
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const screen = currentScreen.name;
const currentRoute = useNavigationStore((state) => state.currentRoute);
const insets = useGlobalSafeAreaInsets();
SearchService.prepareSearch?.();
const allItems = SearchService.getSearchInformation()?.get() || [];
@@ -205,9 +204,9 @@ export const SelectionHeader = React.memo(() => {
size={SIZE.xl}
/>
{screen === "Trash" ||
screen === "Notebooks" ||
screen === "Reminders" ? null : (
{currentRoute === "Trash" ||
currentRoute === "Notebooks" ||
currentRoute === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
@@ -256,17 +255,15 @@ export const SelectionHeader = React.memo(() => {
</>
)}
{screen === "TopicNotes" || screen === "Notebook" ? (
{currentRoute === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentScreen =
useNavigationStore.getState().currentScreen;
if (screen === "Notebook") {
const { focusedRouteId } = useNavigationStore.getState();
if (currentRoute === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
{ type: "notebook", id: focusedRouteId },
item
);
}
@@ -287,9 +284,7 @@ export const SelectionHeader = React.memo(() => {
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
@@ -298,7 +293,7 @@ export const SelectionHeader = React.memo(() => {
/>
) : null}
{screen === "Favorites" ? (
{currentRoute === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
@@ -312,7 +307,7 @@ export const SelectionHeader = React.memo(() => {
/>
) : null}
{screen === "Trash" ? null : (
{currentRoute === "Trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
@@ -328,7 +323,7 @@ export const SelectionHeader = React.memo(() => {
/>
)}
{screen === "Trash" ? (
{currentRoute === "Trash" ? (
<>
<IconButton
customStyle={{

View File

@@ -38,6 +38,7 @@ import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
export const AddNotebookSheet = ({
notebook,
@@ -84,9 +85,12 @@ export const AddNotebookSheet = ({
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
eSendEvent(eOnNotebookUpdated, parentNotebook?.id);
if (notebook) {
eSendEvent(eOnNotebookUpdated, notebook.id);
const parent = await getParentNotebookId(notebook.id);
eSendEvent(eOnNotebookUpdated, parent);
setImmediate(() => {
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}
if (!notebook) {
@@ -136,6 +140,11 @@ export const AddNotebookSheet = ({
onChangeText={(value) => {
title.current = value;
}}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
placeholder="Enter a title"
onSubmit={() => {
descriptionInput.current?.focus();
@@ -160,8 +169,13 @@ export const AddNotebookSheet = ({
);
};
AddNotebookSheet.present = (notebook?: Notebook, parentNotebook?: Notebook) => {
AddNotebookSheet.present = (
notebook?: Notebook,
parentNotebook?: Notebook,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}

View File

@@ -1,29 +0,0 @@
/*
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 { createContext, useContext } from "react";
export const SelectionContext = createContext({
toggleSelection: (item) => {},
deselect: (item) => {},
select: (item) => {},
deselectAll: () => {}
});
export const SelectionProvider = SelectionContext.Provider;
export const useSelectionContext = () => useContext(SelectionContext);

View File

@@ -1,76 +0,0 @@
/*
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 React, { useCallback, useEffect, useRef, useState } from "react";
import { FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
export const FilteredList = ({
data,
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
const query = useRef();
const onChangeText = useCallback(
(value) => {
query.current = value;
try {
if (!value) return setFiltered(data);
const results = db.lookup[itemType + "s"]([...data], value);
setFiltered(results);
} catch (e) {
console.warn(e.message);
}
},
[data, itemType]
);
const onSubmit = async (value) => {
return await onAddItem(value);
};
useEffect(() => {
onChangeText(query.current);
}, [data, onChangeText]);
return (
<FlashList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
onSubmit={onSubmit}
onChangeText={onChangeText}
itemType={itemType}
testID={"list-input" + itemType}
placeholder={`Search or add a new ${itemType}`}
/>
) : null
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
/>
);
};

View File

@@ -17,12 +17,11 @@ 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 { Note, Notebook } from "@notesnook/core";
import { GroupHeader, Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect, useMemo } from "react";
import React, { RefObject, useCallback, useEffect } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
@@ -36,24 +35,53 @@ import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import SheetProvider from "../../sheet-provider";
import { ItemSelection } from "../../../stores/item-selection-store";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"notebook"
)
.get();
const initialSelectionState: ItemSelection = {};
const notebookIds = [
...new Set(relations.map((relation) => relation.fromId))
];
for (const id of notebookIds) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
useNotebookItemSelectionStore.setState({
initialState: initialSelectionState,
selection: { ...initialSelectionState },
multiSelect: relations.length > 1
});
}
/**
* Render all notebooks
* Render sub notebooks
* fix selection, remove topics stuff.
* show already selected notebooks regardless of their level
* show intermediate selection for nested notebooks at all levels.
* @returns
*/
const MoveNoteSheet = ({
note,
actionSheetRef
}: {
note: Note;
note: Note | undefined;
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
@@ -63,72 +91,42 @@ const MoveNoteSheet = ({
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const multiSelect = useNotebookItemSelectionStore(
(state) => state.multiSelect
);
useEffect(() => {
const items = note
? [note.id]
: (selectedItemsList as Note[]).map((note) => note.id);
updateInitialSelectionState(items);
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
useNotebookItemSelectionStore.setState({
initialState: {},
selection: {},
multiSelect: false,
canEnableMultiSelectMode: true
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateItemState = useCallback(function (
item: Notebook,
state: "selected" | "intermediate" | "deselected"
) {
const itemState = { ...useItemSelectionStore.getState().itemState };
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
});
},
[]);
const contextValue = useMemo(
() => ({
toggleSelection: (item: Notebook) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
},
deselect: (item: Notebook) => {
updateItemState(item, "deselected");
},
select: (item: Notebook) => {
updateItemState(item, "selected");
},
deselectAll: () => {
useItemSelectionStore.setState({
itemState: {}
});
}
}),
[updateItemState]
);
}, [note, selectedItemsList]);
const onSave = async () => {
const noteIds = note
? [note.id]
: selectedItemsList.map((n) => (n as Note).id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
for (const id in changedNotebooks) {
const item = await db.notebooks.notebook(id);
if (!item) continue;
if (itemState[id] === "selected") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
if (changedNotebooks[id] === "selected") {
for (const id of noteIds) {
await db.relations.add(item, { id: id, type: "note" });
}
} else if (itemState[id] === "deselected") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
} else if (changedNotebooks[id] === "deselected") {
for (const id of noteIds) {
await db.relations.unlink(item, { id: id, type: "note" });
}
}
}
@@ -141,9 +139,18 @@ const MoveNoteSheet = ({
actionSheetRef.current?.hide();
};
const renderNotebook = useCallback(
({ item, index }: { item: string | GroupHeader; index: number }) =>
(item as GroupHeader).type === "header" ? null : (
<NotebookItem items={notebooks} id={item as string} index={index} />
),
[notebooks]
);
return (
<>
<Dialog context="move_note" />
<SheetProvider context="link-notebooks" />
<View>
<TouchableOpacity
style={{
@@ -204,127 +211,50 @@ const MoveNoteSheet = ({
}}
type="grayAccent"
onPress={() => {
useItemSelectionStore.setState({
itemState: {}
});
const items = note
? [note.id]
: (selectedItemsList as Note[]).map((note) => note.id);
updateInitialSelectionState(items);
}}
/>
</View>
<SelectionProvider value={contextValue}>
<View
<View
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85,
height: 50 * ((notebooks?.ids.length || 0) + 2)
}}
>
<FlashList
data={notebooks?.ids?.filter((id) => typeof id === "string")}
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85,
height: 50 * ((notebooks?.ids.length || 0) + 2)
width: "100%"
}}
>
<FilteredList
ListEmptyComponent={
notebooks?.ids.length ? null : (
<View
style={{
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name="book-outline"
color={colors.primary.icon}
size={100}
/>
<Paragraph style={{ marginBottom: 10 }}>
You do not have any notebooks.
</Paragraph>
</View>
)
}
estimatedItemSize={50}
data={notebooks?.ids.length}
hasHeaderSearch={true}
renderItem={({ item, index }) => (
<ListItem
item={item}
key={item.id}
index={index}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
infoText={
<>
{item.topics.length === 1
? item.topics.length + " topic"
: item.topics.length + " topics"}
</>
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
hasNotes: getSelectedNotesCountInItem(topic) > 0,
style: {
marginBottom: 0,
height: 40
},
onPress: (item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
},
key: item.id,
type: "transparent"
})}
icon={(expanded) => ({
name: expanded ? "chevron-up" : "chevron-down",
color: expanded
? colors.primary.accent
: colors.primary.paragraph
})}
onScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
hasSubList={true}
hasHeaderSearch={false}
type="grayBg"
sublistItemType="topic"
onAddItem={(title) => {
return onAddTopic(title, item);
}}
onAddSublistItem={(item) => {
openAddTopicDialog(item);
}}
onPress={(item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
}}
/>
)}
itemType="notebook"
onAddItem={async (title) => {
return await onAddNotebook(title);
}}
ListFooterComponent={<View style={{ height: 20 }} />}
/>
</View>
</SelectionProvider>
estimatedItemSize={50}
keyExtractor={(item) => item as string}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
MoveNoteSheet.present = (note?: Note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,

View File

@@ -1,88 +0,0 @@
/*
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 React, { useRef, useState } from "react";
import { View } from "react-native";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { useThemeColors } from "@notesnook/theme";
export const ListHeaderInputItem = ({
onSubmit,
onChangeText,
placeholder,
testID
}) => {
const [focused, setFocused] = useState(false);
const { colors } = useThemeColors("sheet");
const [inputValue, setInputValue] = useState();
const inputRef = useRef();
return (
<View
style={{
width: "100%",
marginTop: 10,
marginBottom: 5
}}
>
<Input
fwdRef={inputRef}
onChangeText={(value) => {
setInputValue(value);
onChangeText?.(value);
}}
testID={testID}
blurOnSubmit={false}
onFocusInput={() => {
setFocused(true);
}}
onBlurInput={() => {
setFocused(false);
}}
button={{
icon: inputValue ? "plus" : "magnify",
color: focused ? colors.selected.icon : colors.secondary.icon,
onPress: async () => {
const result = await onSubmit(inputValue);
if (result) {
inputRef.current?.blur();
}
}
}}
placeholder={placeholder}
marginBottom={5}
/>
{inputValue ? (
<View
style={{
backgroundColor: colors.primary.shade,
padding: 5,
borderRadius: 5,
marginBottom: 10
}}
>
<Paragraph color={colors.primary.accent}>
Tap on + to add {`"${inputValue}"`}
</Paragraph>
</View>
) : null}
</View>
);
};

View File

@@ -1,293 +0,0 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const { colors } = useThemeColors("sheet");
useEffect(() => {
onChange?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemState]);
return (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.static.red
: isIntermediate || isSelected
? colors.selected.icon
: colors.primary.icon
}
onPress={() => {
if (multiSelect) return selectItem();
onPress?.(item);
}}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
testID={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
);
};
export const ListItem = ({
item,
index,
icon,
infoText,
hasSubList,
onPress,
onScrollEnd,
getListItems,
style,
type,
sublistItemType,
onAddItem,
getSublistItemProps,
hasHeaderSearch,
onAddSublistItem,
hasNotes,
onChange,
sheetRef
}) => {
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const { colors } = useThemeColors("sheet");
const [expanded, setExpanded] = useState(false);
function selectItem() {
toggleSelection(item);
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
return (
<View
style={{
overflow: "hidden",
marginBottom: 10,
...style
}}
>
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (multiSelect) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
customStyle={{
height: style?.height || 50,
width: "100%",
alignItems: "flex-start"
}}
>
<View
style={{
width: "100%",
height: 50,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
<Heading size={SIZE.md}>{item.title}</Heading>
) : (
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
)}
{infoText ? (
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
{infoText}
</Paragraph>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{showSelectedIndicator ? (
<View
style={{
backgroundColor: colors.primary.accent,
width: 7,
height: 7,
borderRadius: 100,
marginRight: 12
}}
/>
) : null}
{onAddSublistItem ? (
<IconButton
name={"plus"}
testID="add-item-icon"
color={colors.primary.paragraph}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
}}
/>
) : null}
{icon ? (
<IconButton
name={icon(expanded).name}
color={icon(expanded).color}
size={icon(expanded).size || SIZE.xl}
onPress={
hasSubList
? () => setExpanded(!expanded)
: icon(expanded).onPress
}
/>
) : null}
</View>
</View>
</PressableButton>
{expanded && hasSubList ? (
<FilteredList
nestedScrollEnabled
data={getListItems(item)}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
onMomentumScrollEnd={onScrollEnd}
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 250
}}
estimatedItemSize={40}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
renderItem={({ item, index }) => (
<ListItem
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
onAddItem={onAddItem}
/>
) : null}
</View>
);
};

View File

@@ -0,0 +1,236 @@
/*
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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useMemo } from "react";
import { View, useWindowDimensions } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { useTotalNotes } from "../../../hooks/use-db-item";
import { useNotebook } from "../../../hooks/use-notebook";
import useNavigationStore from "../../../stores/use-navigation-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import {
useNotebookExpandedStore,
useNotebookItemSelectionStore
} from "./store";
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
export const NotebookItem = ({
id,
currentLevel = 0,
index,
parent,
items
}: {
id: string;
currentLevel?: number;
index: number;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const { nestedNotebooks, notebook: item } = useNotebook(id, items);
const ids = useMemo(() => (id ? [id] : []), [id]);
const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook");
const screen = useNavigationStore((state) => state.currentRoute);
const { colors } = useThemeColors("sheet");
const selection = useNotebookItemSelectionStore((state) =>
id ? state.selection[id] : undefined
);
const isSelected = selection === "selected";
const isFocused = screen.id === id;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
const onPress = () => {
if (!item) return;
const state = useNotebookItemSelectionStore.getState();
if (isSelected) {
state.markAs(item, !state.initialState[id] ? undefined : "deselected");
return;
}
if (!state.multiSelect) {
const keys = Object.keys(state.selection);
const nextState: any = {};
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
console.log("Single item selection");
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
console.log("Multi item selection");
state.markAs(item, "selected");
}
};
return (
<View
style={{
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
width: "100%"
}}
>
<PressableButton
type={"transparent"}
onLongPress={() => {
if (!item) return;
useNotebookItemSelectionStore.setState({
multiSelect: true
});
useNotebookItemSelectionStore.getState().markAs(item, "selected");
}}
testID={`add-to-notebook-item-${currentLevel}-${index}`}
onPress={onPress}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingLeft: 0,
paddingRight: 12,
borderRadius: 0
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<IconButton
size={SIZE.lg}
color={
isSelected
? colors.selected.icon
: selection === "deselected"
? colors.error.accent
: colors.primary.icon
}
onPress={onPress}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={
selection === "deselected"
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
{nestedNotebooks?.ids.length ? (
<IconButton
size={SIZE.lg}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
}}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={expanded ? "chevron-down" : "chevron-right"}
/>
) : (
<>
<View
style={{
width: 40,
height: 40
}}
/>
</>
)}
<Paragraph
color={
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
}
size={SIZE.sm}
>
{item?.title}{" "}
{totalNotes?.(id) ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{totalNotes(id)}
</Paragraph>
) : null}
</Paragraph>
</View>
<IconButton
name="plus"
customStyle={{
width: 40 * fontScale,
height: 40 * fontScale
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
if (!item) return;
AddNotebookSheet.present(undefined, item, "link-notebooks");
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.primary.icon}
size={SIZE.xl}
/>
</PressableButton>
{!expanded
? null
: nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={id as string}
id={id as string}
index={index}
currentLevel={currentLevel + 1}
items={nestedNotebooks}
parent={{
parent: parent,
item: item
}}
/>
))}
</View>
);
};

View File

@@ -16,27 +16,24 @@ 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 create, { State } from "zustand";
import create from "zustand";
import { createItemSelectionStore } from "../../../stores/item-selection-store";
type SelectionItemState = Record<
string,
"intermediate" | "selected" | "deselected"
>;
export interface SelectionStore extends State {
itemState: SelectionItemState;
setItemState: (state: SelectionItemState) => void;
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
export const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
set({
itemState
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
});
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}
}));
export const useNotebookItemSelectionStore = createItemSelectionStore(true);

View File

@@ -17,8 +17,9 @@ 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 { VirtualizedGrouping } from "@notesnook/core";
import { Tags } from "@notesnook/core/dist/collections/tags";
import { Note, Tag, isGroupHeader } from "@notesnook/core/dist/types";
import { Note, Tag } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
@@ -28,12 +29,21 @@ import React, {
useRef,
useState
} from "react";
import { TextInput, View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { TextInput, View, useWindowDimensions } from "react-native";
import {
ActionSheetRef,
FlashList,
FlatList
} from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import {
ItemSelection,
createItemSelectionStore
} from "../../../stores/item-selection-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useTagStore } from "../../../stores/use-tag-store";
import { SIZE } from "../../../utils/size";
@@ -42,17 +52,40 @@ import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { VirtualizedGrouping } from "@notesnook/core";
function tagHasSomeNotes(tagId: string, noteIds: string[]) {
return db.relations.from({ type: "tag", id: tagId }, "note").has(...noteIds);
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"tag"
)
.get();
const initialSelectionState: ItemSelection = {};
const tagId = [...new Set(relations.map((relation) => relation.fromId))];
for (const id of tagId) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
return initialSelectionState;
}
function tagHasAllNotes(tagId: string, noteIds: string[]) {
return db.relations
.from({ type: "tag", id: tagId }, "note")
.hasAll(...noteIds);
}
const useTagItemSelection = createItemSelectionStore(true);
const ManageTagsSheet = (props: {
notes?: Note[];
@@ -60,12 +93,44 @@ const ManageTagsSheet = (props: {
}) => {
const { colors } = useThemeColors();
const notes = useMemo(() => props.notes || [], [props.notes]);
const tags = useTagStore((state) => state.tags);
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false);
const [queryExists, setQueryExists] = useState(false);
const dimensions = useWindowDimensions();
const refreshSelection = useCallback(() => {
const ids = notes.map((item) => item.id);
updateInitialSelectionState(ids).then((selection) => {
useTagItemSelection.setState({
initialState: selection,
selection: { ...selection }
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [notes, tags]);
const refreshTags = useCallback(() => {
if (query && query.trim() !== "") {
db.lookup.tags(query).then((items) => {
setTags(items);
console.log("searched tags");
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags");
setTags(items);
});
}
}, [query]);
useEffect(() => {
refreshTags();
}, [refreshTags, query]);
useEffect(() => {
refreshSelection();
}, [refreshSelection]);
const checkQueryExists = (query: string) => {
db.tags.all
@@ -114,6 +179,7 @@ const ManageTagsSheet = (props: {
useRelationStore.getState().update();
useTagStore.getState().setTags();
refreshTags();
} catch (e) {
ToastManager.show({
heading: "Cannot add tag",
@@ -126,13 +192,64 @@ const ManageTagsSheet = (props: {
Navigation.queueRoutesForUpdate();
};
const onPress = useCallback(
async (id: string) => {
for (const note of notes) {
try {
if (!id) return;
const isSelected =
useTagItemSelection.getState().initialState[id] === "selected";
if (isSelected) {
await db.relations.unlink(
{
id: id,
type: "tag"
},
note
);
} else {
await db.relations.add(
{
id: id,
type: "tag"
},
note
);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
useRelationStore.getState().update();
refreshTags();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refreshSelection();
},
[notes, refreshSelection, refreshTags]
);
const renderTag = useCallback(
({ item }: { item: string; index: number }) => (
<TagItem
key={item as string}
tags={tags as VirtualizedGrouping<Tag>}
id={item as string}
onPress={onPress}
/>
),
[onPress, tags]
);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%"
maxHeight: dimensions.height * 0.85
}}
>
<Input
@@ -161,31 +278,35 @@ const ManageTagsSheet = (props: {
placeholder="Search or add a tag"
/>
<ScrollView
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
{query && !queryExists ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
{!tags || tags.ids.length === 0 ? (
{query && !queryExists ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
<FlatList
data={tags?.ids?.filter((id) => typeof id === "string") as string[]}
style={{
width: "100%"
}}
keyboardShouldPersistTaps
keyboardDismissMode="interactive"
keyExtractor={(item) => item as string}
renderItem={renderTag}
ListEmptyComponent={
<View
style={{
width: "100%",
@@ -204,19 +325,9 @@ const ManageTagsSheet = (props: {
You do not have any tags.
</Paragraph>
</View>
) : null}
{tags?.ids
.filter((id) => !isGroupHeader(id))
.map((item) => (
<TagItem
key={item as string}
tags={tags}
id={item as string}
notes={notes}
/>
))}
</ScrollView>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
);
};
@@ -233,69 +344,17 @@ export default ManageTagsSheet;
const TagItem = ({
id,
notes,
tags
tags,
onPress
}: {
id: string;
notes: Note[];
tags: VirtualizedGrouping<Tag>;
onPress: (id: string) => void;
}) => {
const { colors } = useThemeColors();
const [tag, setTag] = useState<Tag>();
const [selection, setSelection] = useState({
all: false,
some: false
});
const update = useRelationStore((state) => state.updater);
const [tag] = useDBItem(id, "tag", tags);
const selection = useTagItemSelection((state) => state.selection[id]);
const refresh = useCallback(() => {
tags.item(id).then(async (tag) => {
if (tag?.id) {
setSelection({
all: await tagHasAllNotes(
tag.id,
notes.map((note) => note.id)
),
some: await tagHasSomeNotes(
tag.id,
notes.map((note) => note.id)
)
});
}
setTag(tag);
});
}, [id, tags, notes]);
if (tag?.id !== id) {
refresh();
}
useEffect(() => {
if (tag?.id === id) {
refresh();
}
}, [id, refresh, tag?.id, update]);
const onPress = async () => {
for (const note of notes) {
try {
if (!tag?.id) return;
if (selection.all) {
await db.relations.unlink(tag, note);
} else {
await db.relations.add(tag, note);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
useRelationStore.getState().update();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refresh();
};
return (
<PressableButton
customStyle={{
@@ -304,34 +363,32 @@ const TagItem = ({
justifyContent: "flex-start",
height: 40
}}
onPress={onPress}
onPress={() => onPress(id)}
type="gray"
>
{!tag ? null : (
<IconButton
<Icon
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
onPress={onPress}
onPress={() => onPress(id)}
color={
selection.some || selection.all
selection === "selected" || selection === "intermediate"
? colors.selected.icon
: colors.primary.icon
}
style={{
marginRight: 6
}}
testID={
selection.all
selection === "selected"
? "check-circle-outline"
: selection.some
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection.all
selection === "selected"
? "check-circle-outline"
: selection.some
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -344,7 +401,7 @@ const TagItem = ({
style={{
width: 200,
height: 30,
backgroundColor: colors.secondary.background,
// backgroundColor: colors.secondary.background,
borderRadius: 5
}}
/>

View File

@@ -52,6 +52,24 @@ import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import Sort from "../sort";
const SelectionContext = createContext<{
selection: Notebook[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: Notebook) => void;
}>({
selection: [],
enabled: false,
setEnabled: (_value: boolean) => {},
toggleSelection: (_item: Notebook) => {}
});
const useSelection = () => useContext(SelectionContext);
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
type ConfigItem = { id: string; type: string };
class NotebookSheetConfig {
static storageKey: "$$sp";
@@ -88,8 +106,10 @@ const useNotebookExpandedStore = create<{
export const NotebookSheet = () => {
const [collapsed, setCollapsed] = useState(false);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow = currentScreen.name === "Notebook";
const currentRoute = useNavigationStore((state) => state.currentRoute);
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
const canShow = currentRoute === "Notebook";
const [selection, setSelection] = useState<Notebook[]>([]);
const [enabled, setEnabled] = useState(false);
const { colors } = useThemeColors("sheet");
@@ -103,7 +123,7 @@ export const NotebookSheet = () => {
nestedNotebooks: notebooks,
nestedNotebookNotesCount: totalNotes,
groupOptions
} = useNotebook(currentScreen.name === "Notebook" ? root : undefined);
} = useNotebook(currentRoute === "Notebook" ? root : undefined);
const PLACEHOLDER_DATA = {
heading: "Notebooks",
@@ -158,8 +178,8 @@ export const NotebookSheet = () => {
useEffect(() => {
if (canShow) {
setTimeout(async () => {
const id = currentScreen?.id;
const nextRoot = await findRootNotebookId(id);
if (!focusedRouteId) return;
const nextRoot = await findRootNotebookId(focusedRouteId);
setRoot(nextRoot);
if (nextRoot !== currentItem.current) {
setSelection([]);
@@ -183,7 +203,7 @@ export const NotebookSheet = () => {
setEnabled(false);
ref.current?.hide();
}
}, [canShow, currentScreen?.id, currentScreen.name, onRequestUpdate]);
}, [canShow, currentRoute, onRequestUpdate, focusedRouteId]);
return (
<ActionSheet
@@ -206,7 +226,7 @@ export const NotebookSheet = () => {
NotebookSheetConfig.set(
{
type: "notebook",
id: currentScreen.id as string
id: focusedRouteId as string
},
index
);
@@ -392,24 +412,6 @@ export const NotebookSheet = () => {
);
};
const SelectionContext = createContext<{
selection: Notebook[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: Notebook) => void;
}>({
selection: [],
enabled: false,
setEnabled: (_value: boolean) => {},
toggleSelection: (_item: Notebook) => {}
});
const useSelection = () => useContext(SelectionContext);
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
const NotebookItem = ({
id,
totalNotes,
@@ -430,12 +432,12 @@ const NotebookItem = ({
nestedNotebooks,
notebook: item
} = useNotebook(id, items);
const screen = useNavigationStore((state) => state.currentScreen);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const { colors } = useThemeColors("sheet");
const selection = useSelection();
const isSelected =
selection.selection.findIndex((selected) => selected.id === item?.id) > -1;
const isFocused = screen.id === id;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);

View File

@@ -36,8 +36,9 @@ const Sort = ({ type, screen }) => {
db.settings.getGroupOptions(screen === "Notes" ? "home" : type + "s")
);
const updateGroupOptions = async (_groupOptions) => {
await db.settings.setGroupOptions(type, _groupOptions);
const groupType = screen === "Notes" ? "home" : type + "s";
console.log("updateGroupOptions for group", groupType, "in", screen);
await db.settings.setGroupOptions(groupType, _groupOptions);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);

View File

@@ -66,7 +66,7 @@ const ColorItem = React.memo(
const onHeaderStateChange = useCallback(
(state: any) => {
setTimeout(() => {
let id = state.currentScreen?.id;
let id = state.focusedRouteId;
if (id === item.id) {
setHeaderTextState({ id: state.currentScreen.id });
} else {

View File

@@ -34,10 +34,9 @@ export const MenuItem = React.memo(
function MenuItem({ item, index, testID, rightBtn }) {
const { colors } = useThemeColors();
const [headerTextState, setHeaderTextState] = useState(
useNavigationStore.getState().currentScreen
useNavigationStore.getState().focusedRouteId
);
const screenId = item.name.toLowerCase() + "_navigation";
let isFocused = headerTextState?.id === screenId;
let isFocused = headerTextState?.id === item.name;
const primaryColors = isFocused ? colors.selected : colors.primary;
const _onPress = () => {
@@ -59,9 +58,9 @@ export const MenuItem = React.memo(
const onHeaderStateChange = useCallback(
(state) => {
setTimeout(() => {
let id = state.currentScreen?.id;
if (id === screenId) {
setHeaderTextState({ id: state.currentScreen.id });
let id = state.focusedRouteId;
if (id === item.name) {
setHeaderTextState({ id: state.focusedRouteId });
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
@@ -69,7 +68,7 @@ export const MenuItem = React.memo(
}
}, 300);
},
[headerTextState, screenId]
[headerTextState, item.name]
);
useEffect(() => {

View File

@@ -17,19 +17,19 @@ 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, { useEffect, useRef, useState } from "react";
import { Notebook, Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { FlatList, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { TaggedNotes } from "../../screens/notes/tagged";
import { TopicNotes } from "../../screens/notes/topic-notes";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { db } from "../../common/database";
import { normalize, SIZE } from "../../utils/size";
import { SIZE, normalize } from "../../utils/size";
import { Properties } from "../properties";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
@@ -38,8 +38,6 @@ import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { Notebook, Tag } from "@notesnook/core";
export const TagsSection = React.memo(
function TagsSection() {
@@ -127,7 +125,7 @@ export const PinItem = React.memo(
const onHeaderStateChange = useCallback(
(state: any) => {
setTimeout(() => {
const id = state.currentScreen?.id;
const id = state.focusedRouteId;
if (id === item.id) {
setHeaderTextState({
id

View File

@@ -452,7 +452,7 @@ export const useActions = ({
}
async function showAttachments() {
AttachmentDialog.present(item);
AttachmentDialog.present(item as Note);
}
async function exportNote() {
@@ -486,12 +486,10 @@ export const useActions = ({
};
async function removeNoteFromNotebook() {
const currentScreen = useNavigationStore.getState().currentScreen;
if (currentScreen.name !== "Notebook") return;
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
const { currentRoute, focusedRouteId } = useNavigationStore.getState();
if (currentRoute !== "Notebook" || !focusedRouteId) return;
await db.relations.unlink({ type: "notebook", id: focusedRouteId }, item);
Navigation.queueRoutesForUpdate();
close();
}
@@ -499,7 +497,7 @@ export const useActions = ({
function addTo() {
clearSelection();
setSelectedItem(item);
MoveNoteSheet.present(item);
MoveNoteSheet.present(item as Note);
}
async function addToFavorites() {
@@ -857,11 +855,13 @@ export const useActions = ({
}
useEffect(() => {
const currentScreen = useNavigationStore.getState().currentScreen;
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
const { currentRoute, focusedRouteId } = useNavigationStore.getState();
if (item.type !== "note" || currentRoute !== "Notebook" || !focusedRouteId)
return;
!!db.relations
.to(item, "notebook")
.selector.find((v) => v("id", "==", currentScreen.id))
.selector.find((v) => v("id", "==", focusedRouteId))
.then((notebook) => {
setNoteInCurrentNotebook(!!notebook);
});

View File

@@ -56,9 +56,12 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
const onUpdateItem = (itemId?: string) => {
if (typeof itemId === "string" && itemId !== id) return;
if (!id) {
setItem(undefined);
if (item) {
setItem(undefined);
}
return;
}
console.log("onUpdateItem", id, type);
if (items) {
items.item(id).then((item) => {
@@ -82,7 +85,7 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
return () => {
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
};
}, [id, type]);
}, [id, type, items, item]);
return [
item as ItemTypeKey[T],
@@ -116,6 +119,7 @@ export const useTotalNotes = (
}
setTotalNotesById(totalNotesById);
});
console.log("useTotalNotes.getTotalNotes");
}, [ids, type]);
useEffect(() => {

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
import { useRoute } from "@react-navigation/core";
import useNavigationStore, { RouteName } from "../stores/use-navigation-store";
type NavigationFocus = {
onFocus?: (prev: RefObject<boolean>) => boolean;
@@ -31,6 +33,7 @@ export const useNavigationFocus = (
navigation: NativeStackNavigationProp<Record<string, object | undefined>>,
{ onFocus, onBlur, delay, focusOnInit = true }: NavigationFocus
) => {
const route = useRoute();
const [isFocused, setFocused] = useState(focusOnInit);
const prev = useRef(false);
const isBlurred = useRef(false);
@@ -39,6 +42,12 @@ export const useNavigationFocus = (
setTimeout(
() => {
const shouldFocus = onFocus ? onFocus(prev) : true;
const routeName = route.name?.startsWith("Settings")
? "Settings"
: route.name;
useNavigationStore.getState().update(routeName as RouteName);
if (shouldFocus) {
setFocused(true);
prev.current = true;

View File

@@ -39,17 +39,19 @@ export const useNotebook = (
const onRequestUpdate = React.useCallback(() => {
if (!item || !id) {
console.log("unset notebook");
setNotebooks(undefined);
if (notebooks) {
setNotebooks(undefined);
}
return;
}
console.log("useNotebook.onRequestUpdate", id);
db.relations
.from(item, "notebook")
.selector.sorted(db.settings.getGroupOptions("notebooks"))
.then((notebooks) => {
setNotebooks(notebooks);
});
}, [item, id]);
}, [item, id, notebooks]);
useEffect(() => {
onRequestUpdate();
@@ -75,7 +77,7 @@ export const useNotebook = (
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
};
}, [onUpdate, onRequestUpdate, id]);
}, [onUpdate, onRequestUpdate, id, refresh]);
return {
notebook: item,

View File

@@ -35,7 +35,6 @@ import Notebooks from "../screens/notebooks";
import { ColoredNotes } from "../screens/notes/colored";
import { Monographs } from "../screens/notes/monographs";
import { TaggedNotes } from "../screens/notes/tagged";
import { TopicNotes } from "../screens/notes/topic-notes";
import Reminders from "../screens/reminders";
import { Search } from "../screens/search";
import Settings from "../screens/settings";
@@ -120,44 +119,14 @@ const _Tabs = () => {
<NativeStack.Screen name="Welcome" component={IntroStackNavigator} />
<NativeStack.Screen name="Notes" component={Home} />
<NativeStack.Screen name="Notebooks" component={Notebooks} />
<NativeStack.Screen
options={{ lazy: true }}
name="Favorites"
component={Favorites}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Trash"
component={Trash}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Tags"
component={Tags}
/>
<NativeStack.Screen name="Favorites" component={Favorites} />
<NativeStack.Screen name="Trash" component={Trash} />
<NativeStack.Screen name="Tags" component={Tags} />
<NativeStack.Screen name="Settings" component={Settings} />
<NativeStack.Screen name="TaggedNotes" component={TaggedNotes} />
<NativeStack.Screen name="ColoredNotes" component={ColoredNotes} />
<NativeStack.Screen name="Reminders" component={Reminders} />
<NativeStack.Screen
options={{ lazy: true }}
name="TaggedNotes"
component={TaggedNotes}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="TopicNotes"
component={TopicNotes}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="ColoredNotes"
component={ColoredNotes}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Reminders"
component={Reminders}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Monographs"
initialParams={{
item: { type: "monograph" },
@@ -166,16 +135,8 @@ const _Tabs = () => {
}}
component={Monographs}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Notebook"
component={NotebookScreen}
/>
<NativeStack.Screen
options={{ lazy: true }}
name="Search"
component={Search}
/>
<NativeStack.Screen name="Notebook" component={NotebookScreen} />
<NativeStack.Screen name="Search" component={Search} />
</NativeStack.Navigator>
);
};

View File

@@ -28,6 +28,7 @@ import SettingsService from "../../services/settings";
import { useFavoriteStore } from "../../stores/use-favorite-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { Header } from "../../components/header";
const prepareSearch = () => {
SearchService.update({
placeholder: "Search in favorites",
@@ -50,9 +51,7 @@ export const Favorites = ({
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name
});
useNavigationStore.getState().setFocusedRouteId(route?.name);
SearchService.prepareSearch = prepareSearch;
return !prev?.current;
},
@@ -61,23 +60,43 @@ export const Favorites = ({
});
return (
<DelayLayout wait={loading}>
<List
data={favorites}
dataType="note"
onRefresh={() => {
setFavorites();
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={false}
hasSearch={true}
id={route.name}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
type: "note",
title: route.name,
route: route.name,
ids: favorites?.ids.filter(
(id) => typeof id === "string"
) as string[]
});
}}
renderedInRoute="Favorites"
loading={loading || !isFocused}
placeholder={{
title: "Your favorites",
paragraph: "You have not added any notes to favorites yet.",
loading: "Loading your favorites"
}}
headerTitle="Favorites"
/>
</DelayLayout>
<DelayLayout wait={loading}>
<List
data={favorites}
dataType="note"
onRefresh={() => {
setFavorites();
}}
renderedInRoute="Favorites"
loading={loading || !isFocused}
placeholder={{
title: "Your favorites",
paragraph: "You have not added any notes to favorites yet.",
loading: "Loading your favorites"
}}
headerTitle="Favorites"
/>
</DelayLayout>
</>
);
};

View File

@@ -18,26 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common";
const prepareSearch = () => {
SearchService.update({
placeholder: "Type a keyword to search in notes",
type: "notes",
title: "Notes",
get: () => db.notes?.all
});
};
import useNavigationStore from "../../stores/use-navigation-store";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const notes = useNoteStore((state) => state.notes);
@@ -48,11 +38,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(openEditor);
useNavigationStore.getState().setFocusedRouteId(route.name);
return !prev?.current;
},
onBlur: () => false,
@@ -60,23 +46,41 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
});
return (
<DelayLayout wait={loading} delay={500}>
<List
data={notes}
dataType="note"
renderedInRoute="Notes"
loading={loading || !isFocused}
headerTitle="Notes"
placeholder={{
title: "Notes",
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
loading: "Loading your notes"
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={false}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
type: "note",
title: route.name,
route: route.name
});
}}
id={route.name}
onPressDefaultRightButton={openEditor}
/>
<FloatingButton title="Create a new note" onPress={openEditor} />
</DelayLayout>
<DelayLayout wait={loading} delay={500}>
<List
data={notes}
dataType="note"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={route.name}
placeholder={{
title: route.name?.toLowerCase(),
paragraph: `You have not added any ${route.name.toLowerCase()} yet.`,
button: "Add your first note",
action: openEditor,
loading: "Loading your notes"
}}
/>
<FloatingButton title="Create a new note" onPress={openEditor} />
</DelayLayout>
</>
);
};

View File

@@ -21,6 +21,7 @@ import { Note, Notebook } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
@@ -30,7 +31,6 @@ import {
eUnSubscribeEvent
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore, {
NotebookScreenParams
} from "../../stores/use-navigation-store";
@@ -46,7 +46,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onFocus: () => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
syncWithNavigation();
useNavigationStore.getState().setButtonAction(openEditor);
return false;
},
onBlur: () => {
@@ -56,21 +55,12 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
});
const syncWithNavigation = React.useCallback(() => {
useNavigationStore.getState().update(
{
name: route.name,
title: params.current?.title,
id: params.current?.item?.id,
type: "notebook"
},
params.current?.canGoBack
);
useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id);
setOnFirstSave({
type: "notebook",
id: params.current.item.id
});
SearchService.prepareSearch = prepareSearch;
}, [route.name]);
}, []);
const onRequestUpdate = React.useCallback(
async (data?: NotebookScreenParams) => {
@@ -111,44 +101,25 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
};
}, []);
const prepareSearch = () => {
// SearchService.update({
// placeholder: `Search in "${params.current.title}"`,
// type: "notes",
// title: params.current.title,
// get: () => {
// const notebook = db.notebooks?.notebook(
// params?.current?.item?.id
// )?.data;
// if (!notebook) return [];
// const notes = db.relations?.from(notebook, "note") || [];
// const topicNotes = db.notebooks
// .notebook(notebook.id)
// ?.topics.all.map((topic: Topic) => {
// return db.notes?.topicReferences
// .get(topic.id)
// .map((id: string) => db.notes?.note(id)?.data);
// })
// .flat()
// .filter(
// (topicNote) =>
// notes.findIndex((note) => note?.id !== topicNote?.id) === -1
// ) as Note[];
// return [...notes, ...topicNotes];
// }
// });
};
const PLACEHOLDER_DATA = {
title: params.current.item?.title,
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
loading: "Loading notebook notes"
};
return (
<>
<Header
renderedInRoute={route.name}
title={params.current.item?.title}
canGoBack={params?.current?.canGoBack}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${params.current.item?.title}`,
type: "note",
title: params.current.item?.title,
route: route.name,
ids: notes?.ids.filter((id) => typeof id === "string") as string[]
});
}}
id={params.current.item?.id}
onPressDefaultRightButton={openEditor}
/>
<DelayLayout>
<List
data={notes}
@@ -170,7 +141,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
}
/>
}
placeholder={PLACEHOLDER_DATA}
placeholder={{
title: params.current.item?.title,
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
loading: "Loading notebook notes"
}}
/>
</DelayLayout>
</>
@@ -179,19 +156,11 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
NotebookScreen.navigate = (item: Notebook, canGoBack?: boolean) => {
if (!item) return;
Navigation.navigate<"Notebook">(
{
title: item.title,
name: "Notebook",
id: item.id,
type: "notebook"
},
{
title: item.title,
item: item,
canGoBack
}
);
Navigation.navigate<"Notebook">("Notebook", {
title: item.title,
item: item,
canGoBack
});
};
export default NotebookScreen;

View File

@@ -19,32 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { Config } from "react-native-config";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Walkthrough } from "../../components/walkthroughs";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNotebookStore } from "../../stores/use-notebook-store";
const onPressFloatingButton = () => {
const onButtonPress = () => {
AddNotebookSheet.present();
};
const prepareSearch = () => {
SearchService.update({
placeholder: "Type a keyword to search in notebooks",
type: "notebooks",
title: "Notebooks",
get: () => db.notebooks?.all
});
};
export const Notebooks = ({
navigation,
route
@@ -56,12 +46,7 @@ export const Notebooks = ({
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(onPressFloatingButton);
useNavigationStore.getState().setFocusedRouteId(route.name);
return !prev?.current;
},
onBlur: () => false,
@@ -79,29 +64,47 @@ export const Notebooks = ({
}, [notebooks]);
return (
<DelayLayout delay={1}>
<List
data={notebooks}
dataType="notebook"
renderedInRoute="Notebooks"
loading={!isFocused}
placeholder={{
title: "Your notebooks",
paragraph: "You have not added any notebooks yet.",
button: "Add your first notebook",
action: onPressFloatingButton,
loading: "Loading your notebooks"
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={route.params?.canGoBack}
hasSearch={true}
id={route.name}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
type: "notebook",
title: route.name,
route: route.name
});
}}
headerTitle="Notebooks"
onPressDefaultRightButton={onButtonPress}
/>
{!notebooks || notebooks.ids.length === 0 || !isFocused ? null : (
<FloatingButton
title="Create a new notebook"
onPress={onPressFloatingButton}
<DelayLayout delay={1}>
<List
data={notebooks}
dataType="notebook"
renderedInRoute="Notebooks"
loading={!isFocused}
placeholder={{
title: "Your notebooks",
paragraph: "You have not added any notebooks yet.",
button: "Add your first notebook",
action: onButtonPress,
loading: "Loading your notebooks"
}}
headerTitle="Notebooks"
/>
)}
</DelayLayout>
{!notebooks || notebooks.ids.length === 0 || !isFocused ? null : (
<FloatingButton
title="Create a new notebook"
onPress={onButtonPress}
/>
)}
</DelayLayout>
</>
);
};

View File

@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Color } from "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import NotesPage from ".";
import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor, toCamelCase } from "./common";
import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common";
export const ColoredNotes = ({
navigation,
route
@@ -54,18 +53,9 @@ ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
if (!item) return;
Navigation.navigate<"ColoredNotes">(
{
name: "ColoredNotes",
title: toCamelCase(item.title),
id: item.id,
type: "color",
color: item.title?.toLowerCase()
},
{
item: item,
canGoBack,
title: toCamelCase(item.title)
}
);
Navigation.navigate<"ColoredNotes">("ColoredNotes", {
item: item,
canGoBack,
title: toCamelCase(item.title)
});
};

View File

@@ -29,6 +29,14 @@ import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { editorController, editorState } from "../editor/tiptap/utils";
export const PLACEHOLDER_DATA = {
title: "Your notes",
paragraph: "You have not added any notes yet.",
button: "Add your first Note",
action: openEditor,
loading: "Loading your notes."
};
export function toCamelCase(title: string) {
if (!title) return "";
return title.slice(0, 1).toUpperCase() + title.slice(1);

View File

@@ -17,21 +17,14 @@ 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 {
Color,
GroupedItems,
Item,
Note,
Topic
} from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
import { Color, Note } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { IconButton } from "../../components/ui/icon-button";
import Paragraph from "../../components/ui/typography/paragraph";
import { PlaceholderData } from "../../components/list/empty";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import {
eSubscribeEvent,
@@ -45,39 +38,11 @@ import useNavigationStore, {
RouteName
} from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { SIZE } from "../../utils/size";
import NotebookScreen from "../notebook/index";
import {
openEditor,
openMonographsWebpage,
setOnFirstSave,
toCamelCase
} from "./common";
import { PlaceholderData } from "../../components/list/empty";
import { VirtualizedGrouping } from "@notesnook/core";
import { setOnFirstSave } from "./common";
export const WARNING_DATA = {
title: "Some notes in this topic are not synced"
};
export const PLACEHOLDER_DATA = {
title: "Your notes",
paragraph: "You have not added any notes yet.",
button: "Add your first Note",
action: openEditor,
loading: "Loading your notes."
};
export const MONOGRAPH_PLACEHOLDER_DATA = {
heading: "Your monographs",
paragraph: "You have not published any notes as monographs yet.",
button: "Learn more about monographs",
action: openMonographsWebpage,
loading: "Loading published notes.",
type: "monographs",
buttonIcon: "information-outline"
};
export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
get: (
params: NotesScreenParams,
@@ -93,7 +58,6 @@ export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
function getItemType(routeName: RouteName) {
if (routeName === "TaggedNotes") return "tag";
if (routeName === "ColoredNotes") return "color";
if (routeName === "TopicNotes") return "topic";
if (routeName === "Monographs") return "monograph";
return "note";
}
@@ -110,24 +74,24 @@ const NotesPage = ({
"NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes"
>) => {
const params = useRef<NotesScreenParams>(route?.params);
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const loading = useNoteStore((state) => state.loading);
const [loadingNotes, setLoadingNotes] = useState(true);
const isMonograph = route.name === "Monographs";
// const notebook =
// route.name === "TopicNotes" &&
// params.current.item.type === "topic" &&
// params.current.item.notebookId
// ? db.notebooks?.notebook((params.current.item as Topic).notebookId)?.data
// : null;
const title =
params.current?.item.type === "tag"
? "#" + params.current?.item.title
: params.current?.item.title;
const accentColor =
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined;
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
syncWithNavigation();
if (focusControl) return !prev.current;
return false;
},
@@ -143,10 +107,7 @@ const NotesPage = ({
SearchService.update({
placeholder: `Search in ${item.title}`,
type: "notes",
title:
item.type === "tag"
? "#" + item.title
: toCamelCase((item as Color).title),
title: item.type === "tag" ? "#" + item.title : item.title,
get: () => {
return get(params.current, false);
}
@@ -154,31 +115,16 @@ const NotesPage = ({
}, [get]);
const syncWithNavigation = React.useCallback(() => {
const { item, title } = params.current;
useNavigationStore.getState().update(
{
name: route.name,
title:
route.name === "ColoredNotes" ? toCamelCase(title as string) : title,
id: item?.id,
type: "notes",
notebookId: item.type === "topic" ? item.notebookId : undefined,
color:
item.type === "color" && route.name === "ColoredNotes"
? item.title?.toLowerCase()
: undefined
},
params.current.canGoBack,
rightButtons && rightButtons(params.current)
);
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(onPressFloatingButton);
const { item } = params.current;
useNavigationStore
.getState()
.setFocusedRouteId(params?.current?.item?.id || route.name);
!isMonograph &&
setOnFirstSave({
type: getItemType(route.name),
id: item.id,
notebook: item.type === "topic" ? item.notebookId : undefined
id: item.id
});
}, [
isMonograph,
@@ -192,9 +138,6 @@ const NotesPage = ({
async (data?: NotesScreenParams) => {
const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data;
params.current.title =
params.current.title ||
(params.current.item as Item & { title: string }).title;
const { item } = params.current;
try {
if (isNew) setLoadingNotes(true);
@@ -204,9 +147,8 @@ const NotesPage = ({
)) as VirtualizedGrouping<Note>;
if (
((item.type === "tag" || item.type === "color") &&
(!notes || notes.ids.length === 0)) ||
(item.type === "topic" && !notes)
(item.type === "tag" || item.type === "color") &&
(!notes || notes.ids.length === 0)
) {
return Navigation.goBack();
}
@@ -243,81 +185,51 @@ const NotesPage = ({
}, [onRequestUpdate, route.name]);
return (
<DelayLayout
color={
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined
}
wait={loading || loadingNotes}
>
{/* {route.name === "TopicNotes" ? (
<View
style={{
width: "100%",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center"
}}
>
<Paragraph
onPress={() => {
Navigation.navigate({
name: "Notebooks",
title: "Notebooks"
});
}}
size={SIZE.xs}
>
Notebooks
</Paragraph>
{notebook ? (
<>
<IconButton
name="chevron-right"
size={14}
customStyle={{ width: 25, height: 25 }}
/>
<Paragraph
onPress={() => {
NotebookScreen.navigate(notebook, true);
}}
size={SIZE.xs}
>
{notebook.title}
</Paragraph>
</>
) : null}
</View>
) : null} */}
<List
data={notes}
dataType="note"
onRefresh={onRequestUpdate}
loading={loading || !isFocused}
renderedInRoute="Notes"
headerTitle={params.current.title}
customAccentColor={
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined
<>
<Header
renderedInRoute={route.name}
title={title}
canGoBack={params?.current?.canGoBack}
hasSearch={true}
id={
route.name === "Monographs" ? "Monographs" : params?.current.item?.id
}
placeholder={placeholder}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${title}`,
type: "note",
title: title,
route: route.name,
ids: notes?.ids?.filter((id) => typeof id === "string") as string[]
});
}}
accentColor={accentColor}
onPressDefaultRightButton={onPressFloatingButton}
headerRightButtons={rightButtons?.(params?.current)}
/>
{!isMonograph &&
((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? (
<FloatingButton
color={
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined
}
title="Create a note"
onPress={onPressFloatingButton}
<DelayLayout color={accentColor} wait={loading || loadingNotes}>
<List
data={notes}
dataType="note"
onRefresh={onRequestUpdate}
loading={loading || !isFocused}
renderedInRoute="Notes"
headerTitle={title}
customAccentColor={accentColor}
placeholder={placeholder}
/>
) : null}
</DelayLayout>
{!isMonograph &&
((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? (
<FloatingButton
color={accentColor}
title="Create a note"
onPress={onPressFloatingButton}
/>
) : null}
</DelayLayout>
</>
);
};

View File

@@ -18,12 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import NotesPage from ".";
import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { MonographType } from "../../utils/types";
import { openMonographsWebpage } from "./common";
export const MONOGRAPH_PLACEHOLDER_DATA = {
title: "Your monographs",
paragraph: "You have not published any notes as monographs yet.",
button: "Learn more about monographs",
action: openMonographsWebpage,
loading: "Loading published notes.",
type: "monograph",
buttonIcon: "information-outline"
};
export const Monographs = ({
navigation,
route
@@ -33,7 +43,7 @@ export const Monographs = ({
navigation={navigation}
route={route}
get={Monographs.get}
placeholder={PLACEHOLDER_DATA}
placeholder={MONOGRAPH_PLACEHOLDER_DATA}
onPressFloatingButton={openMonographsWebpage}
canGoBack={route.params?.canGoBack}
focusControl={true}
@@ -49,16 +59,10 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
return await db.monographs.all.grouped(db.settings.getGroupOptions("notes"));
};
Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => {
Navigation.navigate<"Monographs">(
{
name: "Monographs",
type: "monograph"
},
{
item: { type: "monograph" } as any,
canGoBack: canGoBack as boolean,
title: "Monographs"
}
);
Monographs.navigate = (canGoBack?: boolean) => {
Navigation.navigate<"Monographs">("Monographs", {
item: { type: "monograph" } as any,
canGoBack: canGoBack as boolean,
title: "Monographs"
});
};

View File

@@ -19,11 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Tag } from "@notesnook/core/dist/types";
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import NotesPage from ".";
import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor } from "./common";
import { PLACEHOLDER_DATA, openEditor } from "./common";
export const TaggedNotes = ({
navigation,
route
@@ -53,17 +54,9 @@ TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
if (!item) return;
Navigation.navigate<"TaggedNotes">(
{
name: "TaggedNotes",
title: item.title,
id: item.id,
type: "tag"
},
{
item: item,
canGoBack,
title: item.title
}
);
Navigation.navigate<"TaggedNotes">("TaggedNotes", {
item: item,
canGoBack,
title: item.title
});
};

View File

@@ -1,104 +0,0 @@
/*
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 { Topic } from "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database";
import { MoveNotes } from "../../components/sheets/move-notes/movenote";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor } from "./common";
const headerRightButtons = (params: NotesScreenParams) => [
{
title: "Edit topic",
onPress: () => {
const { item } = params;
if (item.type !== "topic") return;
// eSendEvent(eOpenAddTopicDialog, {
// notebookId: item.notebookId,
// toEdit: item
// });
}
},
{
title: "Move notes",
onPress: () => {
const { item } = params;
if (item?.type !== "topic") return;
const notebook = db.notebooks?.notebook(item.notebookId);
if (notebook) {
MoveNotes.present(notebook.data, item);
}
}
}
];
export const TopicNotes = ({
navigation,
route
}: NavigationProps<"TopicNotes">) => {
return (
<>
<NotesPage
navigation={navigation}
route={route}
get={TopicNotes.get}
placeholderData={PLACEHOLDER_DATA}
onPressFloatingButton={openEditor}
rightButtons={headerRightButtons}
canGoBack={route.params?.canGoBack}
focusControl={true}
/>
</>
);
};
TopicNotes.get = (params: NotesScreenParams, grouped = true) => {
const { id, notebookId } = params.item as Topic;
const topic = db.notebooks?.notebook(notebookId)?.topics.topic(id);
if (!topic) {
return [];
}
const notes = topic?.all || [];
return grouped
? groupArray(notes, db.settings.getGroupOptions("notes"))
: notes;
};
TopicNotes.navigate = (item: Topic, canGoBack: boolean) => {
if (!item) return;
Navigation.navigate<"TopicNotes">(
{
name: "TopicNotes",
title: item.title,
id: item.id,
type: "topic",
notebookId: item.notebookId
},
{
item: item,
canGoBack,
title: item.title
}
);
};

View File

@@ -18,37 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import ReminderSheet from "../../components/sheets/reminder";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useReminderStore } from "../../stores/use-reminder-store";
const prepareSearch = () => {
SearchService.update({
placeholder: "Search in reminders",
type: "reminders",
title: "Reminders",
get: () => db.reminders?.all
});
};
const PLACEHOLDER_DATA = {
title: "Your reminders",
paragraph: "You have not set any reminders yet.",
button: "Set a new reminder",
action: () => {
ReminderSheet.present();
},
loading: "Loading reminders"
};
export const Reminders = ({
navigation,
route
@@ -60,13 +40,8 @@ export const Reminders = ({
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name,
beta: true
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(PLACEHOLDER_DATA.action);
useNavigationStore.getState().setFocusedRouteId(route.name);
return !prev?.current;
},
onBlur: () => false,
@@ -74,23 +49,52 @@ export const Reminders = ({
});
return (
<DelayLayout>
<List
data={reminders}
dataType="reminder"
headerTitle="Reminders"
renderedInRoute="Reminders"
loading={!isFocused}
placeholder={PLACEHOLDER_DATA}
/>
<FloatingButton
title="Set a new reminder"
onPress={() => {
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={false}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
type: "reminder",
title: route.name,
route: route.name
});
}}
id={route.name}
onPressDefaultRightButton={() => {
ReminderSheet.present();
}}
/>
</DelayLayout>
<DelayLayout>
<List
data={reminders}
dataType="reminder"
headerTitle="Reminders"
renderedInRoute="Reminders"
loading={!isFocused}
placeholder={{
title: "Your reminders",
paragraph: "You have not set any reminders yet.",
button: "Set a new reminder",
action: () => {
ReminderSheet.present();
},
loading: "Loading reminders"
}}
/>
<FloatingButton
title="Set a new reminder"
onPress={() => {
ReminderSheet.present();
}}
/>
</DelayLayout>
</>
);
};

View File

@@ -1,78 +0,0 @@
/*
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 React, { useEffect } from "react";
import DelayLayout from "../../components/delay-layout";
import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSearchStore } from "../../stores/use-search-store";
import { inputRef } from "../../utils/global-refs";
import { sleep } from "../../utils/time";
export const Search = ({ navigation, route }) => {
const searchResults = useSearchStore((state) => state.searchResults);
const searching = useSearchStore((state) => state.searching);
const searchStatus = useSearchStore((state) => state.searchStatus);
const setSearchResults = useSearchStore((state) => state.setSearchResults);
const setSearchStatus = useSearchStore((state) => state.setSearchStatus);
useNavigationFocus(navigation, {
onFocus: () => {
sleep(300).then(() => inputRef.current?.focus());
useNavigationStore.getState().update({
name: route.name
});
return false;
},
onBlur: () => false
});
useEffect(() => {
return () => {
setSearchResults([]);
setSearchStatus(false, null);
};
}, [setSearchResults, setSearchStatus]);
return (
<DelayLayout wait={searching}>
<List
listData={searchResults}
type="search"
screen="Search"
focused={() => navigation.isFocused()}
placeholderText={"Notes you write appear here"}
jumpToDialog={true}
loading={searching}
CustomHeader={true}
placeholderData={{
heading: "Search",
paragraph:
searchStatus ||
`Type a keyword to search in ${
SearchService.getSearchInformation().title
}`,
button: null,
loading: "Searching..."
}}
/>
</DelayLayout>
);
};

View File

@@ -0,0 +1,84 @@
/*
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 { Item, VirtualizedGrouping } from "@notesnook/core";
import React, { useState } from "react";
import DelayLayout from "../../components/delay-layout";
import List from "../../components/list";
import { NavigationProps } from "../../services/navigation";
import { SearchBar } from "./search-bar";
import { db } from "../../common/database";
export const Search = ({ route }: NavigationProps<"Search">) => {
const [results, setResults] = useState<VirtualizedGrouping<Item>>();
const [loading, setLoading] = useState(false);
const [searchStatus, setSearchStatus] = useState<string>();
const onSearch = async (query: string) => {
if (!query) {
setResults(undefined);
setLoading(false);
setSearchStatus(undefined);
return;
}
try {
setLoading(true);
const type =
route.params.type === "trash"
? "trash"
: ((route.params?.type + "s") as keyof typeof db.lookup);
console.log(
`Searching in ${type} for ${query}`,
route.params?.ids?.length
);
const results = await db.lookup[type](
query,
route.params?.type === "note" ? route.params?.ids : undefined
);
console.log(`Found ${results.ids?.length} results for ${query}`);
setResults(results);
if (results.ids?.length === 0) {
setSearchStatus(`No results found for ${query}`);
} else {
setSearchStatus(undefined);
}
setLoading(false);
} catch (e) {
console.log(e);
}
};
return (
<>
<SearchBar onChangeText={onSearch} loading={loading} />
<List
data={results}
dataType={route.params?.type}
renderedInRoute={route.name}
loading={false}
placeholder={{
title: route.name,
paragraph:
searchStatus ||
`Type a keyword to search in ${route.params?.title}`,
loading: "Searching..."
}}
/>
</>
);
};

View File

@@ -1,148 +0,0 @@
/*
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 React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import { useSearchStore } from "../../stores/use-search-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
export const SearchBar = () => {
const { colors } = useThemeColors();
const [value, setValue] = useState(null);
const inputRef = useRef();
const setSearchResults = useSearchStore((state) => state.setSearchResults);
const setSearchStatus = useSearchStore((state) => state.setSearchStatus);
const searchingRef = useRef(0);
const onClear = () => {
//inputRef.current?.blur();
inputRef.current?.clear();
setValue(0);
SearchService.setTerm(null);
setSearchResults([]);
setSearchStatus(false, null);
};
useEffect(() => {
sleep(300).then(() => {
inputRef.current?.focus();
});
}, []);
const onChangeText = (value) => {
setValue(value);
search(value);
};
const search = (value) => {
clearTimeout(searchingRef.current);
searchingRef.current = setTimeout(async () => {
try {
if (value === "" || !value) {
setSearchResults([]);
setSearchStatus(false, null);
return;
}
if (value?.length > 0) {
SearchService.setTerm(value);
await SearchService.search();
}
} catch (e) {
console.log(e);
ToastManager.show({
heading: "Error occured while searching",
message: e.message,
type: "error"
});
}
}, 300);
};
return (
<View
style={{
height: 50,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
width: "100%"
}}
>
<IconButton
name="arrow-left"
size={SIZE.xl}
top={10}
bottom={10}
onPress={() => {
SearchService.setTerm(null);
Navigation.goBack();
}}
color={colors.primary.paragraph}
type="gray"
customStyle={{
paddingLeft: 0,
marginLeft: 0,
marginRight: 5
}}
/>
<TextInput
ref={inputRef}
testID="search-input"
style={{
fontSize: SIZE.md + 1,
fontFamily: "OpenSans-Regular",
flexGrow: 1,
height: "100%",
color: colors.primary.paragraph
}}
onChangeText={onChangeText}
placeholder="Type a keyword"
textContentType="none"
returnKeyLabel="Search"
returnKeyType="search"
autoCapitalize="none"
autoCorrect={false}
placeholderTextColor={colors.primary.placeholder}
/>
{value && value.length > 0 ? (
<IconButton
name="close"
size={SIZE.md + 2}
top={20}
bottom={20}
right={20}
onPress={onClear}
type="grayBg"
color={colors.primary.icon}
customStyle={{
width: 25,
height: 25
}}
/>
) : null}
</View>
);
};

View File

@@ -0,0 +1,94 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
import Navigation from "../../services/navigation";
import { SIZE } from "../../utils/size";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { DDS } from "../../services/device-detection";
export const SearchBar = ({
onChangeText,
loading
}: {
onChangeText: (value: string) => void;
loading?: boolean;
}) => {
const insets = useGlobalSafeAreaInsets();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const _onChangeText = (value: string) => {
onChangeText(value);
};
return (
<View
style={{
height: 50 + insets.top,
paddingTop: insets.top,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
width: "100%",
paddingHorizontal: 12
}}
>
<IconButton
name="arrow-left"
size={SIZE.xxl}
top={10}
bottom={10}
onPress={() => {
Navigation.goBack();
}}
color={colors.primary.paragraph}
type="gray"
customStyle={{
paddingLeft: 0,
marginLeft: -5,
marginRight: DDS.isLargeTablet() ? 10 : 7
}}
/>
<TextInput
ref={inputRef}
testID="search-input"
style={{
fontSize: SIZE.md + 1,
fontFamily: "OpenSans-Regular",
flexGrow: 1,
height: "100%",
color: colors.primary.paragraph
}}
autoFocus
onChangeText={_onChangeText}
placeholder="Type a keyword"
textContentType="none"
returnKeyLabel="Search"
returnKeyType="search"
autoCapitalize="none"
autoCorrect={false}
placeholderTextColor={colors.primary.placeholder}
/>
</View>
);
};

View File

@@ -71,7 +71,7 @@ export const useDragState = create<DragState>(
presets["custom"] = _data;
db.settings.setToolbarConfig(
useSettingStore.getState().deviceMode || "mobile",
useSettingStore.getState().deviceMode || ("mobile" as any),
{
preset: "custom",
config: clone(_data)
@@ -81,7 +81,7 @@ export const useDragState = create<DragState>(
},
setPreset: (preset) => {
db.settings.setToolbarConfig(
useSettingStore.getState().deviceMode || "mobile",
useSettingStore.getState().deviceMode || ("mobile" as any),
{
preset,
config: preset === "custom" ? clone(get().customPresetData) : []
@@ -99,7 +99,7 @@ export const useDragState = create<DragState>(
const user = await db.user?.getUser();
if (!user) return;
const toolbarConfig = db.settings.getToolbarConfig(
useSettingStore.getState().deviceMode || "mobile"
useSettingStore.getState().deviceMode || ("mobile" as any)
);
if (!toolbarConfig) {
logger.info("DragState", "No user defined toolbar config was found");
@@ -110,25 +110,20 @@ export const useDragState = create<DragState>(
preset: preset,
data:
preset === "custom"
? clone(toolbarConfig?.config)
? clone(toolbarConfig?.config as any[])
: clone(presets[preset]),
customPresetData:
preset === "custom"
? clone(toolbarConfig?.config)
? clone(toolbarConfig?.config as any[])
: clone(presets["custom"])
});
}
}),
{
name: "drag-state-storage", // unique name
getStorage: () => MMKV as StateStorage,
getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => {
return () => {
logger.info(
"DragState",
"rehydrated drag state",
useNoteStore.getState().loading
);
if (!useNoteStore.getState().loading) {
useDragState.getState().init();
} else {

View File

@@ -29,6 +29,7 @@ import { tabBarRef } from "../../utils/global-refs";
import { components } from "./components";
import { SectionItem } from "./section-item";
import { RouteParams, SettingSection } from "./types";
import { Header } from "../../components/header";
const keyExtractor = (item: SettingSection) => item.id;
const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent(
@@ -42,13 +43,7 @@ const Group = ({
useNavigationFocus(navigation, {
onFocus: () => {
tabBarRef.current?.lock();
useNavigationStore.getState().update(
{
name: "SettingsGroup",
title: route.params.name as string
},
true
);
useNavigationStore.getState().setFocusedRouteId("Settings");
return false;
}
});
@@ -62,25 +57,33 @@ const Group = ({
);
return (
<DelayLayout type="settings" delay={1}>
<View
style={{
flex: 1
}}
>
{route.params.sections ? (
<AnimatedKeyboardAvoidingFlatList
entering={FadeInDown}
data={route.params.sections}
keyExtractor={keyExtractor}
renderItem={renderItem}
enableOnAndroid
enableAutomaticScroll
/>
) : null}
{route.params.component ? components[route.params.component] : null}
</View>
</DelayLayout>
<>
<Header
renderedInRoute="Settings"
title={route.params.name as string}
canGoBack={true}
id="Settings"
/>
<DelayLayout type="settings" delay={1}>
<View
style={{
flex: 1
}}
>
{route.params.sections ? (
<AnimatedKeyboardAvoidingFlatList
entering={FadeInDown}
data={route.params.sections}
keyExtractor={keyExtractor}
renderItem={renderItem}
enableOnAndroid
enableAutomaticScroll
/>
) : null}
{route.params.component ? components[route.params.component] : null}
</View>
</DelayLayout>
</>
);
};

View File

@@ -38,6 +38,7 @@ import { SectionGroup } from "./section-group";
import { settingsGroups } from "./settings-data";
import { RouteParams, SettingSection } from "./types";
import SettingsUserSection from "./user-section";
import { Header } from "../../components/header";
const keyExtractor = (item: SettingSection) => item.id;
const Home = ({
@@ -48,9 +49,7 @@ const Home = ({
useNavigationFocus(navigation, {
onFocus: () => {
useNavigationStore.getState().update({
name: "Settings"
});
useNavigationStore.getState().setFocusedRouteId("Settings");
return false;
},
focusOnInit: true
@@ -74,19 +73,17 @@ const Home = ({
}, []);
return (
<DelayLayout delay={300} type="settings">
{loading && (
//@ts-ignore // Migrate to typescript required.
<BaseDialog animated={false} bounce={false} visible={true}>
<View
style={{
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center"
}}
>
<>
<Header
renderedInRoute="Settings"
title="Settings"
canGoBack={false}
id="Settings"
/>
<DelayLayout delay={300} type="settings">
{loading && (
//@ts-ignore // Migrate to typescript required.
<BaseDialog animated={false} bounce={false} visible={true}>
<View
style={{
width: "100%",
@@ -96,45 +93,55 @@ const Home = ({
alignItems: "center"
}}
>
<Heading color={colors.primary.paragraph} size={SIZE.lg}>
Logging out
</Heading>
<Paragraph color={colors.secondary.paragraph}>
Please wait while we log out and clear app data.
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
marginTop: 15
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
<Heading color={colors.primary.paragraph} size={SIZE.lg}>
Logging out
</Heading>
<Paragraph color={colors.secondary.paragraph}>
Please wait while we log out and clear app data.
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
marginTop: 15
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
</View>
</View>
</View>
</BaseDialog>
)}
</BaseDialog>
)}
<Animated.FlatList
entering={FadeInDown}
data={settingsGroups}
windowSize={1}
keyExtractor={keyExtractor}
ListFooterComponent={<View style={{ height: 200 }} />}
renderItem={renderItem}
/>
</DelayLayout>
<Animated.FlatList
entering={FadeInDown}
data={settingsGroups}
windowSize={1}
keyExtractor={keyExtractor}
ListFooterComponent={<View style={{ height: 200 }} />}
renderItem={renderItem}
/>
</DelayLayout>
</>
);
};

View File

@@ -26,34 +26,6 @@ import Home from "./home";
import { RouteParams } from "./types";
const SettingsStack = createNativeStackNavigator<RouteParams>();
// const Home = React.lazy(() => import(/* webpackChunkName: "settings-home" */ './home'));
// const Group = React.lazy(() => import(/* webpackChunkName: "settings-group" */ './group'));
// const Fallback = () => {
// return (
// <>
// <Header />
// <DelayLayout wait={true} type="settings" />
// </>
// );
// };
// const HomeScreen = (props: NativeStackScreenProps<RouteParams, 'SettingsHome'>) => {
// return (
// <React.Suspense fallback={<Fallback />}>
// <Home {...props} />
// </React.Suspense>
// );
// };
// const GroupScreen = (props: NativeStackScreenProps<RouteParams, 'SettingsGroup'>) => {
// return (
// <React.Suspense fallback={<Fallback />}>
// <Group {...props} />
// </React.Suspense>
// );
// };
export const Settings = () => {
const { colors } = useThemeColors();
return (
@@ -62,7 +34,7 @@ export const Settings = () => {
screenListeners={{
focus: (e) => {
if (e.target?.startsWith("SettingsHome-")) {
useNavigationStore.getState().update({ name: "Settings" }, false);
useNavigationStore.getState().update("Settings");
}
}
}}

View File

@@ -18,23 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useTagStore } from "../../stores/use-tag-store";
const prepareSearch = () => {
SearchService.update({
placeholder: "Search in tags",
type: "tags",
title: "Tags",
get: () => db.tags?.all
});
};
import { db } from "../../common/database";
export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
const tags = useTagStore((state) => state.tags);
@@ -44,11 +36,7 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setFocusedRouteId(route.name);
return !prev?.current;
},
onBlur: () => false,
@@ -56,20 +44,36 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
});
return (
<DelayLayout>
<List
data={tags}
dataType="tag"
headerTitle="Tags"
loading={!isFocused}
renderedInRoute="Tags"
placeholder={{
title: "Your tags",
paragraph: "You have not created any tags for your notes yet.",
loading: "Loading your tags."
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={false}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
type: "tag",
title: route.name,
route: route.name
});
}}
/>
</DelayLayout>
<DelayLayout>
<List
data={tags}
dataType="tag"
headerTitle="Tags"
loading={!isFocused}
renderedInRoute="Tags"
placeholder={{
title: "Your tags",
paragraph: "You have not created any tags for your notes yet.",
loading: "Loading your tags."
}}
/>
</DelayLayout>
</>
);
};

View File

@@ -22,22 +22,14 @@ import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout";
import { presentDialog } from "../../components/dialog/functions";
import { Header } from "../../components/header";
import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
const prepareSearch = () => {
SearchService.update({
placeholder: "Search in trash",
type: "trash",
title: "Trash",
get: () => db.trash?.all
});
};
const onPressFloatingButton = () => {
presentDialog({
@@ -81,40 +73,53 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
route.name,
Navigation.routeUpdateFunctions[route.name]
);
useNavigationStore.getState().update({
name: route.name
});
useNavigationStore.getState().setFocusedRouteId(route.name);
if (
!useTrashStore.getState().trash ||
useTrashStore.getState().trash?.ids?.length === 0
) {
useTrashStore.getState().setTrash();
}
SearchService.prepareSearch = prepareSearch;
return false;
},
onBlur: () => false
});
return (
<DelayLayout>
<List
data={trash}
dataType="trash"
renderedInRoute="Trash"
loading={!isFocused}
placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())}
headerTitle="Trash"
<>
<Header
renderedInRoute={route.name}
title={route.name}
canGoBack={false}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
type: "trash",
title: route.name,
route: route.name
});
}}
/>
{trash && trash?.ids?.length !== 0 ? (
<FloatingButton
title="Clear all trash"
onPress={onPressFloatingButton}
alwaysVisible={true}
<DelayLayout>
<List
data={trash}
dataType="trash"
renderedInRoute="Trash"
loading={!isFocused}
placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())}
headerTitle="Trash"
/>
) : null}
</DelayLayout>
{trash && trash?.ids?.length !== 0 ? (
<FloatingButton
title="Clear all trash"
onPress={onPressFloatingButton}
alwaysVisible={true}
/>
) : null}
</DelayLayout>
</>
);
};

View File

@@ -21,7 +21,6 @@ import { StackActions } from "@react-navigation/native";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useFavoriteStore } from "../stores/use-favorite-store";
import useNavigationStore, {
CurrentScreen,
GenericRouteParam,
RouteName,
RouteParams
@@ -34,8 +33,8 @@ import { useTrashStore } from "../stores/use-trash-store";
import { eOnNewTopicAdded } from "../utils/events";
import { rootNavigatorRef, tabBarRef } from "../utils/global-refs";
import { eSendEvent } from "./event-manager";
import SettingsService from "./settings";
import SearchService from "./search";
import SettingsService from "./settings";
/**
* Routes that should be updated on focus
@@ -113,59 +112,35 @@ function queueRoutesForUpdate(...routesToUpdate: RouteName[]) {
routesToUpdate?.length > 0
? routesToUpdate
: (Object.keys(routeNames) as (keyof RouteParams)[]);
const currentScreen = useNavigationStore.getState().currentScreen;
if (routes.indexOf(currentScreen.name) > -1) {
routeUpdateFunctions[currentScreen.name]?.();
clearRouteFromQueue(currentScreen.name);
const currentRoute = useNavigationStore.getState().currentRoute;
if (routes.indexOf(currentRoute) > -1) {
routeUpdateFunctions[currentRoute]?.();
clearRouteFromQueue(currentRoute);
// Remove focused screen from queue
routes.splice(routes.indexOf(currentScreen.name), 1);
routes.splice(routes.indexOf(currentRoute), 1);
}
routesUpdateQueue = routesUpdateQueue.concat(routes);
routesUpdateQueue = [...new Set(routesUpdateQueue)];
}
function navigate<T extends RouteName>(
screen: Omit<Partial<CurrentScreen>, "name"> & {
name: keyof RouteParams;
},
params?: RouteParams[T]
) {
useNavigationStore
.getState()
.update(screen as CurrentScreen, !!params?.canGoBack);
if (screen.name === "Notebook")
routeUpdateFunctions["Notebook"](params || {});
if (screen.name?.endsWith("Notes") && screen.name !== "Notes")
routeUpdateFunctions[screen.name]?.(params || {});
//@ts-ignore Not sure how to fix this for now ignore it.
rootNavigatorRef.current?.navigate<RouteName>(screen.name, params);
function navigate<T extends RouteName>(screen: T, params?: RouteParams[T]) {
rootNavigatorRef.current?.navigate(screen as any, params);
}
function goBack() {
rootNavigatorRef.current?.goBack();
}
function push<T extends RouteName>(
screen: CurrentScreen,
params: RouteParams[T]
) {
useNavigationStore.getState().update(screen, !!params?.canGoBack);
rootNavigatorRef.current?.dispatch(StackActions.push(screen.name, params));
function push<T extends RouteName>(screen: T, params: RouteParams[T]) {
rootNavigatorRef.current?.dispatch(StackActions.push(screen as any, params));
}
function replace<T extends RouteName>(
screen: CurrentScreen,
params: RouteParams[T]
) {
useNavigationStore.getState().update(screen, !!params?.canGoBack);
rootNavigatorRef.current?.dispatch(StackActions.replace(screen.name, params));
function replace<T extends RouteName>(screen: T, params: RouteParams[T]) {
rootNavigatorRef.current?.dispatch(StackActions.replace(screen, params));
}
function popToTop() {
rootNavigatorRef.current?.dispatch(StackActions.popToTop());
useNavigationStore.getState().update({
name: (SettingsService.get().homepage as RouteName) || "Notes"
});
}
function openDrawer() {

View File

@@ -0,0 +1,71 @@
/*
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 { Item } from "@notesnook/core";
import create, { State } from "zustand";
export type SelectionState = "intermediate" | "selected" | "deselected";
export type ItemSelection = Record<string, SelectionState | undefined>;
export interface SelectionStore extends State {
selection: ItemSelection;
setSelection: (state: ItemSelection) => void;
multiSelect: boolean;
toggleMultiSelect: (multiSelect: boolean) => void;
initialState: ItemSelection;
canEnableMultiSelectMode: boolean;
markAs: (item: Item, state: SelectionState | undefined) => void;
reset: () => void;
}
export function createItemSelectionStore(multiSelectMode = false) {
return create<SelectionStore>((set, get) => ({
selection: {},
setSelection: (state) => {
set({
selection: state
});
},
reset: () => {
set({
selection: { ...get().initialState }
});
},
canEnableMultiSelectMode: multiSelectMode,
initialState: {},
markAs: (item, state) => {
set({
selection: {
...get().selection,
[item.id]:
state === "deselected"
? get().initialState === undefined
? undefined
: "deselected"
: state
}
});
},
multiSelect: false,
toggleMultiSelect: () => {
if (!get().canEnableMultiSelectMode) return;
set({
multiSelect: !get().multiSelect
});
}
}));
}

View File

@@ -19,17 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
Color,
ItemType,
Note,
Notebook,
Reminder,
Tag,
Topic,
TrashItem
} from "@notesnook/core/dist/types";
import create, { State } from "zustand";
import { ColorValues } from "../utils/colors";
export type GenericRouteParam = { [name: string]: unknown };
export type GenericRouteParam = undefined;
export type NotebookScreenParams = {
item: Notebook;
@@ -38,7 +38,7 @@ export type NotebookScreenParams = {
};
export type NotesScreenParams = {
item: Note | Notebook | Topic | Tag | Color | TrashItem | Reminder;
item: Note | Notebook | Tag | Color | TrashItem | Reminder;
title: string;
canGoBack?: boolean;
};
@@ -56,90 +56,64 @@ export type AuthParams = {
export type RouteParams = {
Notes: GenericRouteParam;
Notebooks: GenericRouteParam;
Notebooks: {
canGoBack?: boolean;
};
Notebook: NotebookScreenParams;
NotesPage: NotesScreenParams;
Tags: GenericRouteParam;
Favorites: GenericRouteParam;
Trash: GenericRouteParam;
Search: GenericRouteParam;
Search: {
placeholder: string;
type: ItemType;
title: string;
route: RouteName;
ids?: string[];
};
Settings: GenericRouteParam;
TaggedNotes: NotesScreenParams;
ColoredNotes: NotesScreenParams;
TopicNotes: NotesScreenParams;
Monographs: NotesScreenParams;
AppLock: AppLockRouteParams;
Auth: AuthParams;
Reminders: GenericRouteParam;
SettingsGroup: GenericRouteParam;
};
export type RouteName = keyof RouteParams;
export type CurrentScreen = {
name: RouteName;
id: string;
title?: string;
type?: string;
color?: string | null;
notebookId?: string;
beta?: boolean;
};
export type HeaderRightButton = {
title: string;
onPress: () => void;
};
interface NavigationStore extends State {
currentScreen: CurrentScreen;
currentScreenRaw: Partial<CurrentScreen>;
currentRoute: RouteName;
canGoBack?: boolean;
update: (
currentScreen: Omit<Partial<CurrentScreen>, "name"> & {
name: keyof RouteParams;
},
canGoBack?: boolean,
headerRightButtons?: HeaderRightButton[]
) => void;
focusedRouteId?: string;
update: (currentScreen: RouteName) => void;
headerRightButtons?: HeaderRightButton[];
buttonAction: () => void;
setButtonAction: (buttonAction: () => void) => void;
setFocusedRouteId: (id?: string) => void;
}
const useNavigationStore = create<NavigationStore>((set, get) => ({
currentScreen: {
name: "Notes",
id: "notes_navigation",
title: "Notes",
type: "notes"
},
currentScreenRaw: { name: "Notes" },
canGoBack: false,
update: (currentScreen, canGoBack, headerRightButtons) => {
const color =
ColorValues[
currentScreen.color?.toLowerCase() as keyof typeof ColorValues
];
if (
JSON.stringify(currentScreen) === JSON.stringify(get().currentScreenRaw)
)
return;
focusedRouteId: "Notes",
setFocusedRouteId: (id) => {
set({
currentScreen: {
name: currentScreen.name,
id:
currentScreen.id || currentScreen.name.toLowerCase() + "_navigation",
title: currentScreen.title || currentScreen.name,
type: currentScreen.type,
color: color,
notebookId: currentScreen.notebookId,
beta: currentScreen.beta
},
currentScreenRaw: currentScreen,
canGoBack,
headerRightButtons: headerRightButtons
focusedRouteId: id
});
console.log("CurrentRoute ID", id);
},
currentRoute: "Notes",
canGoBack: false,
update: (currentScreen) => {
set({
currentRoute: currentScreen
});
console.log("CurrentRoute", currentScreen);
},
headerRightButtons: [],
buttonAction: () => null,

View File

@@ -1404,6 +1404,7 @@
"@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/toolbar": "^3.12.0",
"@tanstack/react-query": "^4.29.19",
"@tanstack/react-virtual": "^3.0.0-beta.68",
"@theme-ui/color": "^0.14.7",
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
@@ -1439,7 +1440,6 @@
"react-modal": "3.13.1",
"react-qrcode-logo": "^2.2.1",
"react-scroll-sync": "^0.9.0",
"react-virtuoso": "^4.4.2",
"timeago.js": "4.0.2",
"tinycolor2": "^1.6.0",
"w3c-keyname": "^2.2.6",