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"; : "exec";
const result = await this.db.executeAsync(sql, parameters as any[]); 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) if (mode === "query" || !result.insertId)
return { return {
rows: result.rows?._array || [] 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 useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard"; import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store"; import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header"; import SelectionHeader from "../selection-header";
export const Container = ({ children }: PropsWithChildren) => { export const Container = ({ children }: PropsWithChildren) => {
@@ -46,7 +45,6 @@ export const Container = ({ children }: PropsWithChildren) => {
{!introCompleted ? null : ( {!introCompleted ? null : (
<> <>
<SelectionHeader /> <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 React, { useCallback, useEffect, useState } from "react";
import { Platform, StyleSheet, View } from "react-native"; import { Platform, StyleSheet, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { SearchBar } from "../../screens/search/search-bar";
import { import {
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store"; import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { eScrollEvent } from "../../utils/events"; import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus"; import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus"; import { RightMenus } from "./right-menus";
import { Title } from "./title"; 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 { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets(); const insets = useGlobalSafeAreaInsets();
const [hide, setHide] = useState(true); const [borderHidden, setBorderHidden] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode); const selectionMode = useSelectionStore((state) => state.selectionMode);
const currentScreen = useNavigationStore( const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
(state) => state.currentScreen?.name
);
const onScroll = useCallback( 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 (data.y > 150) {
if (!hide) return; if (!borderHidden) return;
setHide(false); setBorderHidden(false);
} else { } else {
if (hide) return; if (borderHidden) return;
setHide(true); setBorderHidden(true);
} }
}, },
[hide] [borderHidden, id, renderedInRoute]
); );
useEffect(() => { useEffect(() => {
@@ -60,9 +91,9 @@ const _Header = () => {
return () => { return () => {
eUnSubscribeEvent(eScrollEvent, onScroll); eUnSubscribeEvent(eScrollEvent, onScroll);
}; };
}, [hide, onScroll]); }, [borderHidden, onScroll]);
return selectionMode ? null : ( return selectionMode && isFocused ? null : (
<> <>
<View <View
style={[ style={[
@@ -72,29 +103,42 @@ const _Header = () => {
backgroundColor: colors.primary.background, backgroundColor: colors.primary.background,
overflow: "hidden", overflow: "hidden",
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: hide borderBottomColor: borderHidden
? "transparent" ? "transparent"
: colors.secondary.background, : colors.secondary.background,
justifyContent: "space-between" justifyContent: "space-between"
} }
]} ]}
> >
{currentScreen === "Search" ? ( <>
<SearchBar /> <View style={styles.leftBtnContainer}>
) : ( <LeftMenus
<> canGoBack={canGoBack}
<View style={styles.leftBtnContainer}> onLeftButtonPress={onLeftMenuButtonPress}
<LeftMenus /> />
<Title />
</View> <Title
<RightMenus /> 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> </View>
</> </>
); );
}; };
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme";
import React from "react"; import React from "react";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import { DDS } from "../../services/device-detection"; import { DDS } from "../../services/device-detection";
import Navigation from "../../services/navigation"; import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store"; import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { tabBarRef } from "../../utils/global-refs"; import { tabBarRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button"; import { IconButton } from "../ui/icon-button";
export const LeftMenus = () => { export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode); const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet"; const isTablet = deviceMode === "tablet";
const onLeftButtonPress = () => { const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) { if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) { if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer(); Navigation.closeDrawer();
@@ -60,7 +66,7 @@ export const LeftMenus = () => {
left={40} left={40}
top={40} top={40}
right={DDS.isLargeTablet() ? 10 : 10} right={DDS.isLargeTablet() ? 10 : 10}
onPress={onLeftButtonPress} onPress={_onLeftButtonPress}
onLongPress={() => { onLongPress={() => {
Navigation.popToTop(); 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 React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native"; import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore //@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu"; import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation"; import Navigation from "../../services/navigation";
import SearchService from "../../services/search"; 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 { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time"; import { sleep } from "../../utils/time";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-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 } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu"); const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode); 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); const menuRef = useRef<Menu>(null);
return ( return (
<View style={styles.rightBtnContainer}> <View style={styles.rightBtnContainer}>
{!currentScreen.startsWith("Settings") ? ( {search ? (
<IconButton <IconButton
onPress={async () => { onPress={onSearch}
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
testID="icon-search" testID="icon-search"
name="magnify" name="magnify"
color={colors.primary.paragraph} color={colors.primary.paragraph}
@@ -63,9 +69,9 @@ export const RightMenus = () => {
{deviceMode !== "mobile" ? ( {deviceMode !== "mobile" ? (
<Button <Button
onPress={buttonAction} onPress={onPressDefaultRightButton}
testID={notesnook.ids.default.addBtn} testID={notesnook.ids.default.addBtn}
icon={currentScreen === "Trash" ? "delete" : "plus"} icon={renderedInRoute === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl} iconSize={SIZE.xl}
type="shade" type="shade"
hitSlop={{ hitSlop={{
@@ -86,7 +92,7 @@ export const RightMenus = () => {
/> />
) : null} ) : null}
{buttons && buttons.length > 0 ? ( {headerRightButtons && headerRightButtons.length > 0 ? (
<Menu <Menu
ref={menuRef} ref={menuRef}
animationDuration={200} animationDuration={200}
@@ -108,7 +114,7 @@ export const RightMenus = () => {
/> />
} }
> >
{buttons.map((item) => ( {headerRightButtons.map((item) => (
<Button <Button
style={{ style={{
width: 150, 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 { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { import {
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { eScrollEvent } from "../../utils/events"; import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import Tag from "../ui/tag"; import Tag from "../ui/tag";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
const titleState: { [id: string]: boolean } = {}; export const Title = ({
title,
export const Title = () => { isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const currentScreen = useNavigationStore((state) => state.currentScreen); const [visible, setVisible] = useState(isHiddenOnRender);
const isNotebook = currentScreen.name === "Notebook"; const isTag = title.startsWith("#");
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 onScroll = useCallback( const onScroll = useCallback(
(data: { x: number; y: number }) => { (data: { x: number; y: number; id?: string; route: string }) => {
if (currentScreen.name !== "Notebook") { if (data.route !== renderedInRoute || data.id !== id) return;
setHide(false);
return;
}
if (data.y > 150) { if (data.y > 150) {
if (!hide) return; if (!visible) return;
titleState[currentScreen.id as string] = false; setVisible(false);
setHide(false);
} else { } else {
if (hide) return; if (visible) return;
titleState[currentScreen.id as string] = true; setVisible(true);
setHide(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(() => { useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll); eSubscribeEvent(eScrollEvent, onScroll);
return () => { return () => {
eUnSubscribeEvent(eScrollEvent, onScroll); eUnSubscribeEvent(eScrollEvent, onScroll);
}; };
}, [hide, onScroll]); }, [visible, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
if (notebook) {
NotebookScreen.navigate(notebook, true);
}
}
return ( return (
<> <>
{!hide && !isHidden ? ( {!visible ? (
<Heading <Heading
onPress={navigateToNotebook}
numberOfLines={1} numberOfLines={1}
size={SIZE.lg} size={SIZE.lg}
style={{ style={{
flexWrap: "wrap", flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0 marginTop: Platform.OS === "ios" ? -1 : 0
}} }}
color={currentScreen.color || colors.primary.heading} color={accentColor || colors.primary.heading}
> >
{isTag ? ( {isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}> <Heading size={SIZE.xl} color={colors.primary.accent}>
# #
</Heading> </Heading>
) : null} ) : null}
{title}{" "} {isTag ? title.slice(1) : title}{" "}
<Tag <Tag
visible={currentScreen.beta} visible={isBeta}
text="BETA" text="BETA"
style={{ style={{
backgroundColor: "transparent" backgroundColor: "transparent"

View File

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

View File

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

View File

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

View File

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

View File

@@ -251,7 +251,7 @@ async function resolveNotes(ids: string[]) {
group.notebooks.map((id) => resolved.notebooks[id]) group.notebooks.map((id) => resolved.notebooks[id])
), ),
attachmentsCount: attachmentsCount:
(await db.attachments?.ofNote(noteId, "all"))?.length || 0 (await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
}; };
} }
return data; 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react"; 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 Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database"; import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook"; import NotebookScreen from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes"; import { eSendEvent, presentSheet } from "../../services/event-manager";
import { import { eClearEditor } from "../../utils/events";
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 { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) { export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
async function getNotebooks(item) { async function getNotebooks(item) {
let filteredNotebooks = []; let filteredNotebooks = [];
const relations = await db.relations.to(note, "notebook").resolve(); const relations = await db.relations.to(note, "notebook").resolve();
filteredNotebooks.push(relations); filteredNotebooks.push(relations);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks; if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
return filteredNotebooks; return filteredNotebooks;
} }
const [noteNotebooks, setNoteNotebooks] = useState([]); const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => { useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks)); getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
}); });
@@ -60,11 +49,6 @@ export default function Notebooks({ note, close, full }) {
NotebookScreen.navigate(item, true); 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) => ( const renderItem = (item) => (
<View <View
key={item.id} key={item.id}
@@ -105,56 +89,6 @@ export default function Notebooks({ note, close, full }) {
> >
{item.title} {item.title}
</Heading> </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>
); );

View File

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

View File

@@ -38,6 +38,7 @@ import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote"; import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events"; import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
export const AddNotebookSheet = ({ export const AddNotebookSheet = ({
notebook, notebook,
@@ -84,9 +85,12 @@ export const AddNotebookSheet = ({
useMenuStore.getState().setMenuPins(); useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate(); Navigation.queueRoutesForUpdate();
useRelationStore.getState().update(); useRelationStore.getState().update();
eSendEvent(eOnNotebookUpdated, parentNotebook?.id);
if (notebook) { if (notebook) {
eSendEvent(eOnNotebookUpdated, notebook.id); const parent = await getParentNotebookId(notebook.id);
eSendEvent(eOnNotebookUpdated, parent);
setImmediate(() => {
eSendEvent(eOnNotebookUpdated, notebook.id);
});
} }
if (!notebook) { if (!notebook) {
@@ -136,6 +140,11 @@ export const AddNotebookSheet = ({
onChangeText={(value) => { onChangeText={(value) => {
title.current = value; title.current = value;
}} }}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
placeholder="Enter a title" placeholder="Enter a title"
onSubmit={() => { onSubmit={() => {
descriptionInput.current?.focus(); 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({ presentSheet({
context: context,
component: (ref, close) => ( component: (ref, close) => (
<AddNotebookSheet <AddNotebookSheet
notebook={notebook} 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/>. 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 { 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 { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet"; import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { eSendEvent, presentSheet } from "../../../services/event-manager"; import { eSendEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
@@ -36,24 +35,53 @@ import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header"; import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context"; import { NotebookItem } from "./notebook-item";
import { FilteredList } from "./filtered-list"; import { useNotebookItemSelectionStore } from "./store";
import { ListItem } from "./list-item"; import SheetProvider from "../../sheet-provider";
import { useItemSelectionStore } from "./store"; 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 = ({ const MoveNoteSheet = ({
note, note,
actionSheetRef actionSheetRef
}: { }: {
note: Note; note: Note | undefined;
actionSheetRef: RefObject<ActionSheetRef>; actionSheetRef: RefObject<ActionSheetRef>;
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
@@ -63,72 +91,42 @@ const MoveNoteSheet = ({
(state) => state.selectedItemsList (state) => state.selectedItemsList
); );
const setNotebooks = useNotebookStore((state) => state.setNotebooks); const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useNotebookItemSelectionStore(
const multiSelect = useItemSelectionStore((state) => state.multiSelect); (state) => state.multiSelect
);
useEffect(() => { useEffect(() => {
const items = note
? [note.id]
: (selectedItemsList as Note[]).map((note) => note.id);
updateInitialSelectionState(items);
return () => { return () => {
useItemSelectionStore.getState().setMultiSelect(false); useNotebookItemSelectionStore.setState({
useItemSelectionStore.getState().setItemState({}); initialState: {},
selection: {},
multiSelect: false,
canEnableMultiSelectMode: true
});
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps }, [note, selectedItemsList]);
}, []);
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]
);
const onSave = async () => { const onSave = async () => {
const noteIds = note const noteIds = note
? [note.id] ? [note.id]
: selectedItemsList.map((n) => (n as 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); const item = await db.notebooks.notebook(id);
if (!item) continue; if (!item) continue;
if (itemState[id] === "selected") { if (changedNotebooks[id] === "selected") {
for (let noteId of noteIds) { for (const id of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" }); await db.relations.add(item, { id: id, type: "note" });
} }
} else if (itemState[id] === "deselected") { } else if (changedNotebooks[id] === "deselected") {
for (let noteId of noteIds) { for (const id of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" }); await db.relations.unlink(item, { id: id, type: "note" });
} }
} }
} }
@@ -141,9 +139,18 @@ const MoveNoteSheet = ({
actionSheetRef.current?.hide(); 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 ( return (
<> <>
<Dialog context="move_note" /> <Dialog context="move_note" />
<SheetProvider context="link-notebooks" />
<View> <View>
<TouchableOpacity <TouchableOpacity
style={{ style={{
@@ -204,127 +211,50 @@ const MoveNoteSheet = ({
}} }}
type="grayAccent" type="grayAccent"
onPress={() => { onPress={() => {
useItemSelectionStore.setState({ const items = note
itemState: {} ? [note.id]
}); : (selectedItemsList as Note[]).map((note) => note.id);
updateInitialSelectionState(items);
}} }}
/> />
</View> </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={{ style={{
paddingHorizontal: 12, width: "100%"
maxHeight: dimensions.height * 0.85,
height: 50 * ((notebooks?.ids.length || 0) + 2)
}} }}
> estimatedItemSize={50}
<FilteredList keyExtractor={(item) => item as string}
ListEmptyComponent={ renderItem={renderNotebook}
notebooks?.ids.length ? null : ( ListEmptyComponent={
<View <View
style={{ style={{
width: "100%", flex: 1,
height: "100%", justifyContent: "center",
justifyContent: "center", alignItems: "center",
alignItems: "center" height: 200
}} }}
> >
<Icon <Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
name="book-outline" </View>
color={colors.primary.icon} }
size={100} ListFooterComponent={<View style={{ height: 50 }} />}
/> />
<Paragraph style={{ marginBottom: 10 }}> </View>
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>
</View> </View>
</> </>
); );
}; };
MoveNoteSheet.present = (note) => { MoveNoteSheet.present = (note?: Note) => {
presentSheet({ presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />, component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false, 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 You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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< export const useNotebookExpandedStore = create<{
string, expanded: {
"intermediate" | "selected" | "deselected" [id: string]: boolean;
>; };
setExpanded: (id: string) => void;
export interface SelectionStore extends State { }>((set, get) => ({
itemState: SelectionItemState; expanded: {},
setItemState: (state: SelectionItemState) => void; setExpanded(id: string) {
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
set({ 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/>. 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 { 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 { useThemeColors } from "@notesnook/theme";
import React, { import React, {
RefObject, RefObject,
@@ -28,12 +29,21 @@ import React, {
useRef, useRef,
useState useState
} from "react"; } from "react";
import { TextInput, View } from "react-native"; import { TextInput, View, useWindowDimensions } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet"; import {
ActionSheetRef,
FlashList,
FlatList
} from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { ToastManager, presentSheet } from "../../../services/event-manager"; import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
import {
ItemSelection,
createItemSelectionStore
} from "../../../stores/item-selection-store";
import { useRelationStore } from "../../../stores/use-relation-store"; import { useRelationStore } from "../../../stores/use-relation-store";
import { useTagStore } from "../../../stores/use-tag-store"; import { useTagStore } from "../../../stores/use-tag-store";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
@@ -42,17 +52,40 @@ import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable"; import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { VirtualizedGrouping } from "@notesnook/core";
function tagHasSomeNotes(tagId: string, noteIds: string[]) { async function updateInitialSelectionState(items: string[]) {
return db.relations.from({ type: "tag", id: tagId }, "note").has(...noteIds); 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[]) { const useTagItemSelection = createItemSelectionStore(true);
return db.relations
.from({ type: "tag", id: tagId }, "note")
.hasAll(...noteIds);
}
const ManageTagsSheet = (props: { const ManageTagsSheet = (props: {
notes?: Note[]; notes?: Note[];
@@ -60,12 +93,44 @@ const ManageTagsSheet = (props: {
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const notes = useMemo(() => props.notes || [], [props.notes]); 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 [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false); const [focus, setFocus] = useState(false);
const [queryExists, setQueryExists] = 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) => { const checkQueryExists = (query: string) => {
db.tags.all db.tags.all
@@ -114,6 +179,7 @@ const ManageTagsSheet = (props: {
useRelationStore.getState().update(); useRelationStore.getState().update();
useTagStore.getState().setTags(); useTagStore.getState().setTags();
refreshTags();
} catch (e) { } catch (e) {
ToastManager.show({ ToastManager.show({
heading: "Cannot add tag", heading: "Cannot add tag",
@@ -126,13 +192,64 @@ const ManageTagsSheet = (props: {
Navigation.queueRoutesForUpdate(); 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 ( return (
<View <View
style={{ style={{
width: "100%", width: "100%",
alignSelf: "center", alignSelf: "center",
paddingHorizontal: 12, paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%" maxHeight: dimensions.height * 0.85
}} }}
> >
<Input <Input
@@ -161,31 +278,35 @@ const ManageTagsSheet = (props: {
placeholder="Search or add a tag" placeholder="Search or add a tag"
/> />
<ScrollView {query && !queryExists ? (
overScrollMode="never" <PressableButton
scrollToOverflowEnabled={false} key={"query_item"}
keyboardDismissMode="none" customStyle={{
keyboardShouldPersistTaps="always" flexDirection: "row",
> marginVertical: 5,
{query && !queryExists ? ( justifyContent: "space-between",
<PressableButton padding: 12
key={"query_item"} }}
customStyle={{ onPress={onSubmit}
flexDirection: "row", type="selected"
marginVertical: 5, >
justifyContent: "space-between", <Heading size={SIZE.sm} color={colors.selected.heading}>
padding: 12 Add {'"' + "#" + query + '"'}
}} </Heading>
onPress={onSubmit} <Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
type="selected" </PressableButton>
> ) : null}
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'} <FlatList
</Heading> data={tags?.ids?.filter((id) => typeof id === "string") as string[]}
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} /> style={{
</PressableButton> width: "100%"
) : null} }}
{!tags || tags.ids.length === 0 ? ( keyboardShouldPersistTaps
keyboardDismissMode="interactive"
keyExtractor={(item) => item as string}
renderItem={renderTag}
ListEmptyComponent={
<View <View
style={{ style={{
width: "100%", width: "100%",
@@ -204,19 +325,9 @@ const ManageTagsSheet = (props: {
You do not have any tags. You do not have any tags.
</Paragraph> </Paragraph>
</View> </View>
) : null} }
ListFooterComponent={<View style={{ height: 50 }} />}
{tags?.ids />
.filter((id) => !isGroupHeader(id))
.map((item) => (
<TagItem
key={item as string}
tags={tags}
id={item as string}
notes={notes}
/>
))}
</ScrollView>
</View> </View>
); );
}; };
@@ -233,69 +344,17 @@ export default ManageTagsSheet;
const TagItem = ({ const TagItem = ({
id, id,
notes, tags,
tags onPress
}: { }: {
id: string; id: string;
notes: Note[];
tags: VirtualizedGrouping<Tag>; tags: VirtualizedGrouping<Tag>;
onPress: (id: string) => void;
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const [tag, setTag] = useState<Tag>(); const [tag] = useDBItem(id, "tag", tags);
const [selection, setSelection] = useState({ const selection = useTagItemSelection((state) => state.selection[id]);
all: false,
some: false
});
const update = useRelationStore((state) => state.updater);
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 ( return (
<PressableButton <PressableButton
customStyle={{ customStyle={{
@@ -304,34 +363,32 @@ const TagItem = ({
justifyContent: "flex-start", justifyContent: "flex-start",
height: 40 height: 40
}} }}
onPress={onPress} onPress={() => onPress(id)}
type="gray" type="gray"
> >
{!tag ? null : ( {!tag ? null : (
<IconButton <Icon
size={22} size={22}
customStyle={{ onPress={() => onPress(id)}
marginRight: 5,
width: 23,
height: 23
}}
onPress={onPress}
color={ color={
selection.some || selection.all selection === "selected" || selection === "intermediate"
? colors.selected.icon ? colors.selected.icon
: colors.primary.icon : colors.primary.icon
} }
style={{
marginRight: 6
}}
testID={ testID={
selection.all selection === "selected"
? "check-circle-outline" ? "check-circle-outline"
: selection.some : selection === "intermediate"
? "minus-circle-outline" ? "minus-circle-outline"
: "checkbox-blank-circle-outline" : "checkbox-blank-circle-outline"
} }
name={ name={
selection.all selection === "selected"
? "check-circle-outline" ? "check-circle-outline"
: selection.some : selection === "intermediate"
? "minus-circle-outline" ? "minus-circle-outline"
: "checkbox-blank-circle-outline" : "checkbox-blank-circle-outline"
} }
@@ -344,7 +401,7 @@ const TagItem = ({
style={{ style={{
width: 200, width: 200,
height: 30, height: 30,
backgroundColor: colors.secondary.background, // backgroundColor: colors.secondary.background,
borderRadius: 5 borderRadius: 5
}} }}
/> />

View File

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

View File

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

View File

@@ -34,10 +34,9 @@ export const MenuItem = React.memo(
function MenuItem({ item, index, testID, rightBtn }) { function MenuItem({ item, index, testID, rightBtn }) {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const [headerTextState, setHeaderTextState] = useState( const [headerTextState, setHeaderTextState] = useState(
useNavigationStore.getState().currentScreen useNavigationStore.getState().focusedRouteId
); );
const screenId = item.name.toLowerCase() + "_navigation"; let isFocused = headerTextState?.id === item.name;
let isFocused = headerTextState?.id === screenId;
const primaryColors = isFocused ? colors.selected : colors.primary; const primaryColors = isFocused ? colors.selected : colors.primary;
const _onPress = () => { const _onPress = () => {
@@ -59,9 +58,9 @@ export const MenuItem = React.memo(
const onHeaderStateChange = useCallback( const onHeaderStateChange = useCallback(
(state) => { (state) => {
setTimeout(() => { setTimeout(() => {
let id = state.currentScreen?.id; let id = state.focusedRouteId;
if (id === screenId) { if (id === item.name) {
setHeaderTextState({ id: state.currentScreen.id }); setHeaderTextState({ id: state.focusedRouteId });
} else { } else {
if (headerTextState !== null) { if (headerTextState !== null) {
setHeaderTextState(null); setHeaderTextState(null);
@@ -69,7 +68,7 @@ export const MenuItem = React.memo(
} }
}, 300); }, 300);
}, },
[headerTextState, screenId] [headerTextState, item.name]
); );
useEffect(() => { 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/>. 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 { FlatList, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook"; import NotebookScreen from "../../screens/notebook";
import { TaggedNotes } from "../../screens/notes/tagged"; import { TaggedNotes } from "../../screens/notes/tagged";
import { TopicNotes } from "../../screens/notes/topic-notes";
import Navigation from "../../services/navigation"; import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store"; import { useMenuStore } from "../../stores/use-menu-store";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store"; import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme"; import { SIZE, normalize } from "../../utils/size";
import { db } from "../../common/database";
import { normalize, SIZE } from "../../utils/size";
import { Properties } from "../properties"; import { Properties } from "../properties";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Notice } from "../ui/notice"; import { Notice } from "../ui/notice";
@@ -38,8 +38,6 @@ import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet"; import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph"; import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { Notebook, Tag } from "@notesnook/core";
export const TagsSection = React.memo( export const TagsSection = React.memo(
function TagsSection() { function TagsSection() {
@@ -127,7 +125,7 @@ export const PinItem = React.memo(
const onHeaderStateChange = useCallback( const onHeaderStateChange = useCallback(
(state: any) => { (state: any) => {
setTimeout(() => { setTimeout(() => {
const id = state.currentScreen?.id; const id = state.focusedRouteId;
if (id === item.id) { if (id === item.id) {
setHeaderTextState({ setHeaderTextState({
id id

View File

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

View File

@@ -56,9 +56,12 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
const onUpdateItem = (itemId?: string) => { const onUpdateItem = (itemId?: string) => {
if (typeof itemId === "string" && itemId !== id) return; if (typeof itemId === "string" && itemId !== id) return;
if (!id) { if (!id) {
setItem(undefined); if (item) {
setItem(undefined);
}
return; return;
} }
console.log("onUpdateItem", id, type);
if (items) { if (items) {
items.item(id).then((item) => { items.item(id).then((item) => {
@@ -82,7 +85,7 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
return () => { return () => {
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem); eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
}; };
}, [id, type]); }, [id, type, items, item]);
return [ return [
item as ItemTypeKey[T], item as ItemTypeKey[T],
@@ -116,6 +119,7 @@ export const useTotalNotes = (
} }
setTotalNotesById(totalNotesById); setTotalNotesById(totalNotesById);
}); });
console.log("useTotalNotes.getTotalNotes");
}, [ids, type]); }, [ids, type]);
useEffect(() => { 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 { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RefObject, useCallback, useEffect, useRef, useState } from "react"; import { RefObject, useCallback, useEffect, useRef, useState } from "react";
import { useRoute } from "@react-navigation/core";
import useNavigationStore, { RouteName } from "../stores/use-navigation-store";
type NavigationFocus = { type NavigationFocus = {
onFocus?: (prev: RefObject<boolean>) => boolean; onFocus?: (prev: RefObject<boolean>) => boolean;
@@ -31,6 +33,7 @@ export const useNavigationFocus = (
navigation: NativeStackNavigationProp<Record<string, object | undefined>>, navigation: NativeStackNavigationProp<Record<string, object | undefined>>,
{ onFocus, onBlur, delay, focusOnInit = true }: NavigationFocus { onFocus, onBlur, delay, focusOnInit = true }: NavigationFocus
) => { ) => {
const route = useRoute();
const [isFocused, setFocused] = useState(focusOnInit); const [isFocused, setFocused] = useState(focusOnInit);
const prev = useRef(false); const prev = useRef(false);
const isBlurred = useRef(false); const isBlurred = useRef(false);
@@ -39,6 +42,12 @@ export const useNavigationFocus = (
setTimeout( setTimeout(
() => { () => {
const shouldFocus = onFocus ? onFocus(prev) : true; const shouldFocus = onFocus ? onFocus(prev) : true;
const routeName = route.name?.startsWith("Settings")
? "Settings"
: route.name;
useNavigationStore.getState().update(routeName as RouteName);
if (shouldFocus) { if (shouldFocus) {
setFocused(true); setFocused(true);
prev.current = true; prev.current = true;

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import SettingsService from "../../services/settings";
import { useFavoriteStore } from "../../stores/use-favorite-store"; import { useFavoriteStore } from "../../stores/use-favorite-store";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store"; import { useNoteStore } from "../../stores/use-notes-store";
import { Header } from "../../components/header";
const prepareSearch = () => { const prepareSearch = () => {
SearchService.update({ SearchService.update({
placeholder: "Search in favorites", placeholder: "Search in favorites",
@@ -50,9 +51,7 @@ export const Favorites = ({
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId(route?.name);
name: route.name
});
SearchService.prepareSearch = prepareSearch; SearchService.prepareSearch = prepareSearch;
return !prev?.current; return !prev?.current;
}, },
@@ -61,23 +60,43 @@ export const Favorites = ({
}); });
return ( return (
<DelayLayout wait={loading}> <>
<List <Header
data={favorites} renderedInRoute={route.name}
dataType="note" title={route.name}
onRefresh={() => { canGoBack={false}
setFavorites(); 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 React from "react";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button"; import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store"; import { useNoteStore } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common"; import { openEditor } from "../notes/common";
import useNavigationStore from "../../stores/use-navigation-store";
const prepareSearch = () => {
SearchService.update({
placeholder: "Type a keyword to search in notes",
type: "notes",
title: "Notes",
get: () => db.notes?.all
});
};
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const notes = useNoteStore((state) => state.notes); const notes = useNoteStore((state) => state.notes);
@@ -48,11 +38,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId(route.name);
name: route.name
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(openEditor);
return !prev?.current; return !prev?.current;
}, },
onBlur: () => false, onBlur: () => false,
@@ -60,23 +46,41 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
}); });
return ( return (
<DelayLayout wait={loading} delay={500}> <>
<List <Header
data={notes} renderedInRoute={route.name}
dataType="note" title={route.name}
renderedInRoute="Notes" canGoBack={false}
loading={loading || !isFocused} hasSearch={true}
headerTitle="Notes" onSearch={() => {
placeholder={{ Navigation.push("Search", {
title: "Notes", placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
paragraph: "You have not added any notes yet.", type: "note",
button: "Add your first note", title: route.name,
action: openEditor, route: route.name
loading: "Loading your notes" });
}} }}
id={route.name}
onPressDefaultRightButton={openEditor}
/> />
<FloatingButton title="Create a new note" onPress={openEditor} /> <DelayLayout wait={loading} delay={500}>
</DelayLayout> <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 React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database"; import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { NotebookHeader } from "../../components/list-items/headers/notebook-header"; import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
import { AddNotebookSheet } from "../../components/sheets/add-notebook"; import { AddNotebookSheet } from "../../components/sheets/add-notebook";
@@ -30,7 +31,6 @@ import {
eUnSubscribeEvent eUnSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore, { import useNavigationStore, {
NotebookScreenParams NotebookScreenParams
} from "../../stores/use-navigation-store"; } from "../../stores/use-navigation-store";
@@ -46,7 +46,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onFocus: () => { onFocus: () => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate); Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
syncWithNavigation(); syncWithNavigation();
useNavigationStore.getState().setButtonAction(openEditor);
return false; return false;
}, },
onBlur: () => { onBlur: () => {
@@ -56,21 +55,12 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
}); });
const syncWithNavigation = React.useCallback(() => { const syncWithNavigation = React.useCallback(() => {
useNavigationStore.getState().update( useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id);
{
name: route.name,
title: params.current?.title,
id: params.current?.item?.id,
type: "notebook"
},
params.current?.canGoBack
);
setOnFirstSave({ setOnFirstSave({
type: "notebook", type: "notebook",
id: params.current.item.id id: params.current.item.id
}); });
SearchService.prepareSearch = prepareSearch; }, []);
}, [route.name]);
const onRequestUpdate = React.useCallback( const onRequestUpdate = React.useCallback(
async (data?: NotebookScreenParams) => { 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 ( 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> <DelayLayout>
<List <List
data={notes} 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> </DelayLayout>
</> </>
@@ -179,19 +156,11 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
NotebookScreen.navigate = (item: Notebook, canGoBack?: boolean) => { NotebookScreen.navigate = (item: Notebook, canGoBack?: boolean) => {
if (!item) return; if (!item) return;
Navigation.navigate<"Notebook">( Navigation.navigate<"Notebook">("Notebook", {
{ title: item.title,
title: item.title, item: item,
name: "Notebook", canGoBack
id: item.id, });
type: "notebook"
},
{
title: item.title,
item: item,
canGoBack
}
);
}; };
export default NotebookScreen; 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 React, { useEffect } from "react";
import { Config } from "react-native-config"; import { Config } from "react-native-config";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button"; import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { AddNotebookSheet } from "../../components/sheets/add-notebook"; import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Walkthrough } from "../../components/walkthroughs"; import { Walkthrough } from "../../components/walkthroughs";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useNotebookStore } from "../../stores/use-notebook-store"; import { useNotebookStore } from "../../stores/use-notebook-store";
const onPressFloatingButton = () => { const onButtonPress = () => {
AddNotebookSheet.present(); 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 = ({ export const Notebooks = ({
navigation, navigation,
route route
@@ -56,12 +46,7 @@ export const Notebooks = ({
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId(route.name);
name: route.name
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(onPressFloatingButton);
return !prev?.current; return !prev?.current;
}, },
onBlur: () => false, onBlur: () => false,
@@ -79,29 +64,47 @@ export const Notebooks = ({
}, [notebooks]); }, [notebooks]);
return ( return (
<DelayLayout delay={1}> <>
<List <Header
data={notebooks} renderedInRoute={route.name}
dataType="notebook" title={route.name}
renderedInRoute="Notebooks" canGoBack={route.params?.canGoBack}
loading={!isFocused} hasSearch={true}
placeholder={{ id={route.name}
title: "Your notebooks", onSearch={() => {
paragraph: "You have not added any notebooks yet.", Navigation.push("Search", {
button: "Add your first notebook", placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
action: onPressFloatingButton, type: "notebook",
loading: "Loading your notebooks" title: route.name,
route: route.name
});
}} }}
headerTitle="Notebooks" onPressDefaultRightButton={onButtonPress}
/> />
<DelayLayout delay={1}>
{!notebooks || notebooks.ids.length === 0 || !isFocused ? null : ( <List
<FloatingButton data={notebooks}
title="Create a new notebook" dataType="notebook"
onPress={onPressFloatingButton} 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 { Color } from "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import React from "react"; import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from "."; import NotesPage from ".";
import { db } from "../../common/database"; import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store"; import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor, toCamelCase } from "./common"; import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common";
export const ColoredNotes = ({ export const ColoredNotes = ({
navigation, navigation,
route route
@@ -54,18 +53,9 @@ ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
ColoredNotes.navigate = (item: Color, canGoBack: boolean) => { ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
if (!item) return; if (!item) return;
Navigation.navigate<"ColoredNotes">( Navigation.navigate<"ColoredNotes">("ColoredNotes", {
{ item: item,
name: "ColoredNotes", canGoBack,
title: toCamelCase(item.title), title: toCamelCase(item.title)
id: item.id, });
type: "color",
color: item.title?.toLowerCase()
},
{
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 { tabBarRef } from "../../utils/global-refs";
import { editorController, editorState } from "../editor/tiptap/utils"; 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) { export function toCamelCase(title: string) {
if (!title) return ""; if (!title) return "";
return title.slice(0, 1).toUpperCase() + title.slice(1); 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { import { VirtualizedGrouping } from "@notesnook/core";
Color, import { Color, Note } from "@notesnook/core/dist/types";
GroupedItems,
Item,
Note,
Topic
} from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react"; 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 { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { IconButton } from "../../components/ui/icon-button"; import { PlaceholderData } from "../../components/list/empty";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { import {
eSubscribeEvent, eSubscribeEvent,
@@ -45,39 +38,11 @@ import useNavigationStore, {
RouteName RouteName
} from "../../stores/use-navigation-store"; } from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store"; import { useNoteStore } from "../../stores/use-notes-store";
import { SIZE } from "../../utils/size"; import { setOnFirstSave } from "./common";
import NotebookScreen from "../notebook/index";
import {
openEditor,
openMonographsWebpage,
setOnFirstSave,
toCamelCase
} from "./common";
import { PlaceholderData } from "../../components/list/empty";
import { VirtualizedGrouping } from "@notesnook/core";
export const WARNING_DATA = { export const WARNING_DATA = {
title: "Some notes in this topic are not synced" 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> { export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
get: ( get: (
params: NotesScreenParams, params: NotesScreenParams,
@@ -93,7 +58,6 @@ export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
function getItemType(routeName: RouteName) { function getItemType(routeName: RouteName) {
if (routeName === "TaggedNotes") return "tag"; if (routeName === "TaggedNotes") return "tag";
if (routeName === "ColoredNotes") return "color"; if (routeName === "ColoredNotes") return "color";
if (routeName === "TopicNotes") return "topic";
if (routeName === "Monographs") return "monograph"; if (routeName === "Monographs") return "monograph";
return "note"; return "note";
} }
@@ -110,24 +74,24 @@ const NotesPage = ({
"NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes" "NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes"
>) => { >) => {
const params = useRef<NotesScreenParams>(route?.params); const params = useRef<NotesScreenParams>(route?.params);
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>(); const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const loading = useNoteStore((state) => state.loading); const loading = useNoteStore((state) => state.loading);
const [loadingNotes, setLoadingNotes] = useState(true); const [loadingNotes, setLoadingNotes] = useState(true);
const isMonograph = route.name === "Monographs"; const isMonograph = route.name === "Monographs";
const title =
// const notebook = params.current?.item.type === "tag"
// route.name === "TopicNotes" && ? "#" + params.current?.item.title
// params.current.item.type === "topic" && : params.current?.item.title;
// params.current.item.notebookId const accentColor =
// ? db.notebooks?.notebook((params.current.item as Topic).notebookId)?.data route.name === "ColoredNotes"
// : null; ? (params.current?.item as Color)?.colorCode
: undefined;
const isFocused = useNavigationFocus(navigation, { const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => { onFocus: (prev) => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate); Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
syncWithNavigation(); syncWithNavigation();
if (focusControl) return !prev.current; if (focusControl) return !prev.current;
return false; return false;
}, },
@@ -143,10 +107,7 @@ const NotesPage = ({
SearchService.update({ SearchService.update({
placeholder: `Search in ${item.title}`, placeholder: `Search in ${item.title}`,
type: "notes", type: "notes",
title: title: item.type === "tag" ? "#" + item.title : item.title,
item.type === "tag"
? "#" + item.title
: toCamelCase((item as Color).title),
get: () => { get: () => {
return get(params.current, false); return get(params.current, false);
} }
@@ -154,31 +115,16 @@ const NotesPage = ({
}, [get]); }, [get]);
const syncWithNavigation = React.useCallback(() => { const syncWithNavigation = React.useCallback(() => {
const { item, title } = params.current; const { item } = params.current;
useNavigationStore.getState().update(
{ useNavigationStore
name: route.name, .getState()
title: .setFocusedRouteId(params?.current?.item?.id || route.name);
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);
!isMonograph && !isMonograph &&
setOnFirstSave({ setOnFirstSave({
type: getItemType(route.name), type: getItemType(route.name),
id: item.id, id: item.id
notebook: item.type === "topic" ? item.notebookId : undefined
}); });
}, [ }, [
isMonograph, isMonograph,
@@ -192,9 +138,6 @@ const NotesPage = ({
async (data?: NotesScreenParams) => { async (data?: NotesScreenParams) => {
const isNew = data && data?.item?.id !== params.current?.item?.id; const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data; if (data) params.current = data;
params.current.title =
params.current.title ||
(params.current.item as Item & { title: string }).title;
const { item } = params.current; const { item } = params.current;
try { try {
if (isNew) setLoadingNotes(true); if (isNew) setLoadingNotes(true);
@@ -204,9 +147,8 @@ const NotesPage = ({
)) as VirtualizedGrouping<Note>; )) as VirtualizedGrouping<Note>;
if ( if (
((item.type === "tag" || item.type === "color") && (item.type === "tag" || item.type === "color") &&
(!notes || notes.ids.length === 0)) || (!notes || notes.ids.length === 0)
(item.type === "topic" && !notes)
) { ) {
return Navigation.goBack(); return Navigation.goBack();
} }
@@ -243,81 +185,51 @@ const NotesPage = ({
}, [onRequestUpdate, route.name]); }, [onRequestUpdate, route.name]);
return ( return (
<DelayLayout <>
color={ <Header
route.name === "ColoredNotes" renderedInRoute={route.name}
? (params.current?.item as Color)?.colorCode title={title}
: undefined canGoBack={params?.current?.canGoBack}
} hasSearch={true}
wait={loading || loadingNotes} id={
> route.name === "Monographs" ? "Monographs" : params?.current.item?.id
{/* {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
} }
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 && <DelayLayout color={accentColor} wait={loading || loadingNotes}>
((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? ( <List
<FloatingButton data={notes}
color={ dataType="note"
route.name === "ColoredNotes" onRefresh={onRequestUpdate}
? (params.current?.item as Color)?.colorCode loading={loading || !isFocused}
: undefined renderedInRoute="Notes"
} headerTitle={title}
title="Create a note" customAccentColor={accentColor}
onPress={onPressFloatingButton} 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 React from "react";
import NotesPage, { PLACEHOLDER_DATA } from "."; import NotesPage from ".";
import { db } from "../../common/database"; import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store"; import { NotesScreenParams } from "../../stores/use-navigation-store";
import { MonographType } from "../../utils/types";
import { openMonographsWebpage } from "./common"; 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 = ({ export const Monographs = ({
navigation, navigation,
route route
@@ -33,7 +43,7 @@ export const Monographs = ({
navigation={navigation} navigation={navigation}
route={route} route={route}
get={Monographs.get} get={Monographs.get}
placeholder={PLACEHOLDER_DATA} placeholder={MONOGRAPH_PLACEHOLDER_DATA}
onPressFloatingButton={openMonographsWebpage} onPressFloatingButton={openMonographsWebpage}
canGoBack={route.params?.canGoBack} canGoBack={route.params?.canGoBack}
focusControl={true} focusControl={true}
@@ -49,16 +59,10 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
return await db.monographs.all.grouped(db.settings.getGroupOptions("notes")); return await db.monographs.all.grouped(db.settings.getGroupOptions("notes"));
}; };
Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => { Monographs.navigate = (canGoBack?: boolean) => {
Navigation.navigate<"Monographs">( Navigation.navigate<"Monographs">("Monographs", {
{ item: { type: "monograph" } as any,
name: "Monographs", canGoBack: canGoBack as boolean,
type: "monograph" title: "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 { Tag } from "@notesnook/core/dist/types";
import React from "react"; import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from "."; import NotesPage from ".";
import { db } from "../../common/database"; import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store"; import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor } from "./common"; import { PLACEHOLDER_DATA, openEditor } from "./common";
export const TaggedNotes = ({ export const TaggedNotes = ({
navigation, navigation,
route route
@@ -53,17 +54,9 @@ TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => { TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
if (!item) return; if (!item) return;
Navigation.navigate<"TaggedNotes">( Navigation.navigate<"TaggedNotes">("TaggedNotes", {
{ item: item,
name: "TaggedNotes", canGoBack,
title: item.title, title: item.title
id: item.id, });
type: "tag"
},
{
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 React from "react";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button"; import { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import ReminderSheet from "../../components/sheets/reminder"; import ReminderSheet from "../../components/sheets/reminder";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useReminderStore } from "../../stores/use-reminder-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 = ({ export const Reminders = ({
navigation, navigation,
route route
@@ -60,13 +40,8 @@ export const Reminders = ({
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({
name: route.name,
beta: true
});
SearchService.prepareSearch = prepareSearch; useNavigationStore.getState().setFocusedRouteId(route.name);
useNavigationStore.getState().setButtonAction(PLACEHOLDER_DATA.action);
return !prev?.current; return !prev?.current;
}, },
onBlur: () => false, onBlur: () => false,
@@ -74,23 +49,52 @@ export const Reminders = ({
}); });
return ( return (
<DelayLayout> <>
<List <Header
data={reminders} renderedInRoute={route.name}
dataType="reminder" title={route.name}
headerTitle="Reminders" canGoBack={false}
renderedInRoute="Reminders" hasSearch={true}
loading={!isFocused} onSearch={() => {
placeholder={PLACEHOLDER_DATA} Navigation.push("Search", {
/> placeholder: `Type a keyword to search in ${route.name}`,
type: "reminder",
<FloatingButton title: route.name,
title="Set a new reminder" route: route.name
onPress={() => { });
}}
id={route.name}
onPressDefaultRightButton={() => {
ReminderSheet.present(); 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; presets["custom"] = _data;
db.settings.setToolbarConfig( db.settings.setToolbarConfig(
useSettingStore.getState().deviceMode || "mobile", useSettingStore.getState().deviceMode || ("mobile" as any),
{ {
preset: "custom", preset: "custom",
config: clone(_data) config: clone(_data)
@@ -81,7 +81,7 @@ export const useDragState = create<DragState>(
}, },
setPreset: (preset) => { setPreset: (preset) => {
db.settings.setToolbarConfig( db.settings.setToolbarConfig(
useSettingStore.getState().deviceMode || "mobile", useSettingStore.getState().deviceMode || ("mobile" as any),
{ {
preset, preset,
config: preset === "custom" ? clone(get().customPresetData) : [] config: preset === "custom" ? clone(get().customPresetData) : []
@@ -99,7 +99,7 @@ export const useDragState = create<DragState>(
const user = await db.user?.getUser(); const user = await db.user?.getUser();
if (!user) return; if (!user) return;
const toolbarConfig = db.settings.getToolbarConfig( const toolbarConfig = db.settings.getToolbarConfig(
useSettingStore.getState().deviceMode || "mobile" useSettingStore.getState().deviceMode || ("mobile" as any)
); );
if (!toolbarConfig) { if (!toolbarConfig) {
logger.info("DragState", "No user defined toolbar config was found"); logger.info("DragState", "No user defined toolbar config was found");
@@ -110,25 +110,20 @@ export const useDragState = create<DragState>(
preset: preset, preset: preset,
data: data:
preset === "custom" preset === "custom"
? clone(toolbarConfig?.config) ? clone(toolbarConfig?.config as any[])
: clone(presets[preset]), : clone(presets[preset]),
customPresetData: customPresetData:
preset === "custom" preset === "custom"
? clone(toolbarConfig?.config) ? clone(toolbarConfig?.config as any[])
: clone(presets["custom"]) : clone(presets["custom"])
}); });
} }
}), }),
{ {
name: "drag-state-storage", // unique name name: "drag-state-storage", // unique name
getStorage: () => MMKV as StateStorage, getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => { onRehydrateStorage: () => {
return () => { return () => {
logger.info(
"DragState",
"rehydrated drag state",
useNoteStore.getState().loading
);
if (!useNoteStore.getState().loading) { if (!useNoteStore.getState().loading) {
useDragState.getState().init(); useDragState.getState().init();
} else { } else {

View File

@@ -29,6 +29,7 @@ import { tabBarRef } from "../../utils/global-refs";
import { components } from "./components"; import { components } from "./components";
import { SectionItem } from "./section-item"; import { SectionItem } from "./section-item";
import { RouteParams, SettingSection } from "./types"; import { RouteParams, SettingSection } from "./types";
import { Header } from "../../components/header";
const keyExtractor = (item: SettingSection) => item.id; const keyExtractor = (item: SettingSection) => item.id;
const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent( const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent(
@@ -42,13 +43,7 @@ const Group = ({
useNavigationFocus(navigation, { useNavigationFocus(navigation, {
onFocus: () => { onFocus: () => {
tabBarRef.current?.lock(); tabBarRef.current?.lock();
useNavigationStore.getState().update( useNavigationStore.getState().setFocusedRouteId("Settings");
{
name: "SettingsGroup",
title: route.params.name as string
},
true
);
return false; return false;
} }
}); });
@@ -62,25 +57,33 @@ const Group = ({
); );
return ( return (
<DelayLayout type="settings" delay={1}> <>
<View <Header
style={{ renderedInRoute="Settings"
flex: 1 title={route.params.name as string}
}} canGoBack={true}
> id="Settings"
{route.params.sections ? ( />
<AnimatedKeyboardAvoidingFlatList <DelayLayout type="settings" delay={1}>
entering={FadeInDown} <View
data={route.params.sections} style={{
keyExtractor={keyExtractor} flex: 1
renderItem={renderItem} }}
enableOnAndroid >
enableAutomaticScroll {route.params.sections ? (
/> <AnimatedKeyboardAvoidingFlatList
) : null} entering={FadeInDown}
{route.params.component ? components[route.params.component] : null} data={route.params.sections}
</View> keyExtractor={keyExtractor}
</DelayLayout> 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 { settingsGroups } from "./settings-data";
import { RouteParams, SettingSection } from "./types"; import { RouteParams, SettingSection } from "./types";
import SettingsUserSection from "./user-section"; import SettingsUserSection from "./user-section";
import { Header } from "../../components/header";
const keyExtractor = (item: SettingSection) => item.id; const keyExtractor = (item: SettingSection) => item.id;
const Home = ({ const Home = ({
@@ -48,9 +49,7 @@ const Home = ({
useNavigationFocus(navigation, { useNavigationFocus(navigation, {
onFocus: () => { onFocus: () => {
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId("Settings");
name: "Settings"
});
return false; return false;
}, },
focusOnInit: true focusOnInit: true
@@ -74,19 +73,17 @@ const Home = ({
}, []); }, []);
return ( return (
<DelayLayout delay={300} type="settings"> <>
{loading && ( <Header
//@ts-ignore // Migrate to typescript required. renderedInRoute="Settings"
<BaseDialog animated={false} bounce={false} visible={true}> title="Settings"
<View canGoBack={false}
style={{ id="Settings"
width: "100%", />
height: "100%", <DelayLayout delay={300} type="settings">
backgroundColor: colors.primary.background, {loading && (
justifyContent: "center", //@ts-ignore // Migrate to typescript required.
alignItems: "center" <BaseDialog animated={false} bounce={false} visible={true}>
}}
>
<View <View
style={{ style={{
width: "100%", width: "100%",
@@ -96,45 +93,55 @@ const Home = ({
alignItems: "center" 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 <View
style={{ style={{
flexDirection: "row", width: "100%",
width: 100, height: "100%",
marginTop: 15 backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center"
}} }}
> >
<ProgressBarComponent <Heading color={colors.primary.paragraph} size={SIZE.lg}>
height={5} Logging out
width={100} </Heading>
animated={true} <Paragraph color={colors.secondary.paragraph}>
useNativeDriver Please wait while we log out and clear app data.
indeterminate </Paragraph>
indeterminateAnimationDuration={2000} <View
unfilledColor={colors.secondary.background} style={{
color={colors.primary.accent} flexDirection: "row",
borderWidth={0} 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> </View>
</View> </BaseDialog>
</BaseDialog> )}
)}
<Animated.FlatList <Animated.FlatList
entering={FadeInDown} entering={FadeInDown}
data={settingsGroups} data={settingsGroups}
windowSize={1} windowSize={1}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
ListFooterComponent={<View style={{ height: 200 }} />} ListFooterComponent={<View style={{ height: 200 }} />}
renderItem={renderItem} renderItem={renderItem}
/> />
</DelayLayout> </DelayLayout>
</>
); );
}; };

View File

@@ -26,34 +26,6 @@ import Home from "./home";
import { RouteParams } from "./types"; import { RouteParams } from "./types";
const SettingsStack = createNativeStackNavigator<RouteParams>(); 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 = () => { export const Settings = () => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
return ( return (
@@ -62,7 +34,7 @@ export const Settings = () => {
screenListeners={{ screenListeners={{
focus: (e) => { focus: (e) => {
if (e.target?.startsWith("SettingsHome-")) { 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 React from "react";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useTagStore } from "../../stores/use-tag-store"; import { useTagStore } from "../../stores/use-tag-store";
const prepareSearch = () => { import { db } from "../../common/database";
SearchService.update({
placeholder: "Search in tags",
type: "tags",
title: "Tags",
get: () => db.tags?.all
});
};
export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => { export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
const tags = useTagStore((state) => state.tags); const tags = useTagStore((state) => state.tags);
@@ -44,11 +36,7 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId(route.name);
name: route.name
});
SearchService.prepareSearch = prepareSearch;
return !prev?.current; return !prev?.current;
}, },
onBlur: () => false, onBlur: () => false,
@@ -56,20 +44,36 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
}); });
return ( return (
<DelayLayout> <>
<List <Header
data={tags} renderedInRoute={route.name}
dataType="tag" title={route.name}
headerTitle="Tags" canGoBack={false}
loading={!isFocused} hasSearch={true}
renderedInRoute="Tags" onSearch={() => {
placeholder={{ Navigation.push("Search", {
title: "Your tags", placeholder: `Type a keyword to search in ${route.name}`,
paragraph: "You have not created any tags for your notes yet.", type: "tag",
loading: "Loading your tags." 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 { FloatingButton } from "../../components/container/floating-button";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { presentDialog } from "../../components/dialog/functions"; import { presentDialog } from "../../components/dialog/functions";
import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager"; import { ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store"; import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store"; import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-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 = () => { const onPressFloatingButton = () => {
presentDialog({ presentDialog({
@@ -81,40 +73,53 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
route.name, route.name,
Navigation.routeUpdateFunctions[route.name] Navigation.routeUpdateFunctions[route.name]
); );
useNavigationStore.getState().update({ useNavigationStore.getState().setFocusedRouteId(route.name);
name: route.name
});
if ( if (
!useTrashStore.getState().trash || !useTrashStore.getState().trash ||
useTrashStore.getState().trash?.ids?.length === 0 useTrashStore.getState().trash?.ids?.length === 0
) { ) {
useTrashStore.getState().setTrash(); useTrashStore.getState().setTrash();
} }
SearchService.prepareSearch = prepareSearch;
return false; return false;
}, },
onBlur: () => false onBlur: () => false
}); });
return ( return (
<DelayLayout> <>
<List <Header
data={trash} renderedInRoute={route.name}
dataType="trash" title={route.name}
renderedInRoute="Trash" canGoBack={false}
loading={!isFocused} hasSearch={true}
placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())} onSearch={() => {
headerTitle="Trash" Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
type: "trash",
title: route.name,
route: route.name
});
}}
/> />
<DelayLayout>
{trash && trash?.ids?.length !== 0 ? ( <List
<FloatingButton data={trash}
title="Clear all trash" dataType="trash"
onPress={onPressFloatingButton} renderedInRoute="Trash"
alwaysVisible={true} 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 { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useFavoriteStore } from "../stores/use-favorite-store"; import { useFavoriteStore } from "../stores/use-favorite-store";
import useNavigationStore, { import useNavigationStore, {
CurrentScreen,
GenericRouteParam, GenericRouteParam,
RouteName, RouteName,
RouteParams RouteParams
@@ -34,8 +33,8 @@ import { useTrashStore } from "../stores/use-trash-store";
import { eOnNewTopicAdded } from "../utils/events"; import { eOnNewTopicAdded } from "../utils/events";
import { rootNavigatorRef, tabBarRef } from "../utils/global-refs"; import { rootNavigatorRef, tabBarRef } from "../utils/global-refs";
import { eSendEvent } from "./event-manager"; import { eSendEvent } from "./event-manager";
import SettingsService from "./settings";
import SearchService from "./search"; import SearchService from "./search";
import SettingsService from "./settings";
/** /**
* Routes that should be updated on focus * Routes that should be updated on focus
@@ -113,59 +112,35 @@ function queueRoutesForUpdate(...routesToUpdate: RouteName[]) {
routesToUpdate?.length > 0 routesToUpdate?.length > 0
? routesToUpdate ? routesToUpdate
: (Object.keys(routeNames) as (keyof RouteParams)[]); : (Object.keys(routeNames) as (keyof RouteParams)[]);
const currentScreen = useNavigationStore.getState().currentScreen; const currentRoute = useNavigationStore.getState().currentRoute;
if (routes.indexOf(currentScreen.name) > -1) { if (routes.indexOf(currentRoute) > -1) {
routeUpdateFunctions[currentScreen.name]?.(); routeUpdateFunctions[currentRoute]?.();
clearRouteFromQueue(currentScreen.name); clearRouteFromQueue(currentRoute);
// Remove focused screen from queue // Remove focused screen from queue
routes.splice(routes.indexOf(currentScreen.name), 1); routes.splice(routes.indexOf(currentRoute), 1);
} }
routesUpdateQueue = routesUpdateQueue.concat(routes); routesUpdateQueue = routesUpdateQueue.concat(routes);
routesUpdateQueue = [...new Set(routesUpdateQueue)]; routesUpdateQueue = [...new Set(routesUpdateQueue)];
} }
function navigate<T extends RouteName>( function navigate<T extends RouteName>(screen: T, params?: RouteParams[T]) {
screen: Omit<Partial<CurrentScreen>, "name"> & { rootNavigatorRef.current?.navigate(screen as any, params);
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 goBack() { function goBack() {
rootNavigatorRef.current?.goBack(); rootNavigatorRef.current?.goBack();
} }
function push<T extends RouteName>( function push<T extends RouteName>(screen: T, params: RouteParams[T]) {
screen: CurrentScreen, rootNavigatorRef.current?.dispatch(StackActions.push(screen as any, params));
params: RouteParams[T]
) {
useNavigationStore.getState().update(screen, !!params?.canGoBack);
rootNavigatorRef.current?.dispatch(StackActions.push(screen.name, params));
} }
function replace<T extends RouteName>( function replace<T extends RouteName>(screen: T, params: RouteParams[T]) {
screen: CurrentScreen, rootNavigatorRef.current?.dispatch(StackActions.replace(screen, params));
params: RouteParams[T]
) {
useNavigationStore.getState().update(screen, !!params?.canGoBack);
rootNavigatorRef.current?.dispatch(StackActions.replace(screen.name, params));
} }
function popToTop() { function popToTop() {
rootNavigatorRef.current?.dispatch(StackActions.popToTop()); rootNavigatorRef.current?.dispatch(StackActions.popToTop());
useNavigationStore.getState().update({
name: (SettingsService.get().homepage as RouteName) || "Notes"
});
} }
function openDrawer() { 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 { import {
Color, Color,
ItemType,
Note, Note,
Notebook, Notebook,
Reminder, Reminder,
Tag, Tag,
Topic,
TrashItem TrashItem
} from "@notesnook/core/dist/types"; } from "@notesnook/core/dist/types";
import create, { State } from "zustand"; import create, { State } from "zustand";
import { ColorValues } from "../utils/colors"; import { ColorValues } from "../utils/colors";
export type GenericRouteParam = { [name: string]: unknown }; export type GenericRouteParam = undefined;
export type NotebookScreenParams = { export type NotebookScreenParams = {
item: Notebook; item: Notebook;
@@ -38,7 +38,7 @@ export type NotebookScreenParams = {
}; };
export type NotesScreenParams = { export type NotesScreenParams = {
item: Note | Notebook | Topic | Tag | Color | TrashItem | Reminder; item: Note | Notebook | Tag | Color | TrashItem | Reminder;
title: string; title: string;
canGoBack?: boolean; canGoBack?: boolean;
}; };
@@ -56,90 +56,64 @@ export type AuthParams = {
export type RouteParams = { export type RouteParams = {
Notes: GenericRouteParam; Notes: GenericRouteParam;
Notebooks: GenericRouteParam; Notebooks: {
canGoBack?: boolean;
};
Notebook: NotebookScreenParams; Notebook: NotebookScreenParams;
NotesPage: NotesScreenParams; NotesPage: NotesScreenParams;
Tags: GenericRouteParam; Tags: GenericRouteParam;
Favorites: GenericRouteParam; Favorites: GenericRouteParam;
Trash: GenericRouteParam; Trash: GenericRouteParam;
Search: GenericRouteParam; Search: {
placeholder: string;
type: ItemType;
title: string;
route: RouteName;
ids?: string[];
};
Settings: GenericRouteParam; Settings: GenericRouteParam;
TaggedNotes: NotesScreenParams; TaggedNotes: NotesScreenParams;
ColoredNotes: NotesScreenParams; ColoredNotes: NotesScreenParams;
TopicNotes: NotesScreenParams; TopicNotes: NotesScreenParams;
Monographs: NotesScreenParams; Monographs: NotesScreenParams;
AppLock: AppLockRouteParams; AppLock: AppLockRouteParams;
Auth: AuthParams;
Reminders: GenericRouteParam; Reminders: GenericRouteParam;
SettingsGroup: GenericRouteParam; SettingsGroup: GenericRouteParam;
}; };
export type RouteName = keyof RouteParams; 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 = { export type HeaderRightButton = {
title: string; title: string;
onPress: () => void; onPress: () => void;
}; };
interface NavigationStore extends State { interface NavigationStore extends State {
currentScreen: CurrentScreen; currentRoute: RouteName;
currentScreenRaw: Partial<CurrentScreen>;
canGoBack?: boolean; canGoBack?: boolean;
update: ( focusedRouteId?: string;
currentScreen: Omit<Partial<CurrentScreen>, "name"> & { update: (currentScreen: RouteName) => void;
name: keyof RouteParams;
},
canGoBack?: boolean,
headerRightButtons?: HeaderRightButton[]
) => void;
headerRightButtons?: HeaderRightButton[]; headerRightButtons?: HeaderRightButton[];
buttonAction: () => void; buttonAction: () => void;
setButtonAction: (buttonAction: () => void) => void; setButtonAction: (buttonAction: () => void) => void;
setFocusedRouteId: (id?: string) => void;
} }
const useNavigationStore = create<NavigationStore>((set, get) => ({ const useNavigationStore = create<NavigationStore>((set, get) => ({
currentScreen: { focusedRouteId: "Notes",
name: "Notes", setFocusedRouteId: (id) => {
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;
set({ set({
currentScreen: { focusedRouteId: id
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
}); });
console.log("CurrentRoute ID", id);
},
currentRoute: "Notes",
canGoBack: false,
update: (currentScreen) => {
set({
currentRoute: currentScreen
});
console.log("CurrentRoute", currentScreen);
}, },
headerRightButtons: [], headerRightButtons: [],
buttonAction: () => null, buttonAction: () => null,

View File

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