diff --git a/apps/mobile/app/common/database/sqlite.kysely.ts b/apps/mobile/app/common/database/sqlite.kysely.ts index 6c19f2144..1ac2fb407 100644 --- a/apps/mobile/app/common/database/sqlite.kysely.ts +++ b/apps/mobile/app/common/database/sqlite.kysely.ts @@ -108,7 +108,7 @@ class RNSqliteConnection implements DatabaseConnection { : "exec"; const result = await this.db.executeAsync(sql, parameters as any[]); - console.log("SQLITE result:", result?.rows?._array); + // console.log("SQLITE result:", result?.rows?._array); if (mode === "query" || !result.insertId) return { rows: result.rows?._array || [] diff --git a/apps/mobile/app/components/container/index.tsx b/apps/mobile/app/components/container/index.tsx index e15361aa9..1ca6c6f29 100644 --- a/apps/mobile/app/components/container/index.tsx +++ b/apps/mobile/app/components/container/index.tsx @@ -22,7 +22,6 @@ import { KeyboardAvoidingView, Platform } from "react-native"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard"; import { useSettingStore } from "../../stores/use-setting-store"; -import { Header } from "../header"; import SelectionHeader from "../selection-header"; export const Container = ({ children }: PropsWithChildren) => { @@ -46,7 +45,6 @@ export const Container = ({ children }: PropsWithChildren) => { {!introCompleted ? null : ( <> -
)} diff --git a/apps/mobile/app/components/header/index.tsx b/apps/mobile/app/components/header/index.tsx index 99e981a78..3c811a366 100644 --- a/apps/mobile/app/components/header/index.tsx +++ b/apps/mobile/app/components/header/index.tsx @@ -20,39 +20,70 @@ along with this program. If not, see . import React, { useCallback, useEffect, useState } from "react"; import { Platform, StyleSheet, View } from "react-native"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; -import { SearchBar } from "../../screens/search/search-bar"; import { eSubscribeEvent, eUnSubscribeEvent } from "../../services/event-manager"; -import useNavigationStore from "../../stores/use-navigation-store"; import { useSelectionStore } from "../../stores/use-selection-store"; import { useThemeColors } from "@notesnook/theme"; import { eScrollEvent } from "../../utils/events"; import { LeftMenus } from "./left-menus"; import { RightMenus } from "./right-menus"; import { Title } from "./title"; +import useNavigationStore, { + RouteName +} from "../../stores/use-navigation-store"; -const _Header = () => { +type HeaderRightButton = { + title: string; + onPress: () => void; +}; + +export const Header = ({ + renderedInRoute, + onLeftMenuButtonPress, + title, + titleHiddenOnRender, + headerRightButtons, + id, + accentColor, + isBeta, + canGoBack, + onPressDefaultRightButton, + hasSearch, + onSearch +}: { + onLeftMenuButtonPress?: () => void; + renderedInRoute: RouteName; + id?: string; + title: string; + headerRightButtons?: HeaderRightButton[]; + titleHiddenOnRender?: boolean; + accentColor?: string; + isBeta?: boolean; + canGoBack?: boolean; + onPressDefaultRightButton?: () => void; + hasSearch?: boolean; + onSearch?: () => void; +}) => { const { colors } = useThemeColors(); const insets = useGlobalSafeAreaInsets(); - const [hide, setHide] = useState(true); + const [borderHidden, setBorderHidden] = useState(true); const selectionMode = useSelectionStore((state) => state.selectionMode); - const currentScreen = useNavigationStore( - (state) => state.currentScreen?.name - ); + const isFocused = useNavigationStore((state) => state.focusedRouteId === id); const onScroll = useCallback( - (data: { x: number; y: number }) => { + (data: { x: number; y: number; id?: string; route: string }) => { + if (data.route !== renderedInRoute || data.id !== id) return; if (data.y > 150) { - if (!hide) return; - setHide(false); + if (!borderHidden) return; + setBorderHidden(false); } else { - if (hide) return; - setHide(true); + if (borderHidden) return; + setBorderHidden(true); } }, - [hide] + [borderHidden, id, renderedInRoute] ); useEffect(() => { @@ -60,9 +91,9 @@ const _Header = () => { return () => { eUnSubscribeEvent(eScrollEvent, onScroll); }; - }, [hide, onScroll]); + }, [borderHidden, onScroll]); - return selectionMode ? null : ( + return selectionMode && isFocused ? null : ( <> { backgroundColor: colors.primary.background, overflow: "hidden", borderBottomWidth: 1, - borderBottomColor: hide + borderBottomColor: borderHidden ? "transparent" : colors.secondary.background, justifyContent: "space-between" } ]} > - {currentScreen === "Search" ? ( - - ) : ( - <> - - - - </View> - <RightMenus /> - </> - )} + <> + <View style={styles.leftBtnContainer}> + <LeftMenus + canGoBack={canGoBack} + onLeftButtonPress={onLeftMenuButtonPress} + /> + + <Title + isHiddenOnRender={titleHiddenOnRender} + renderedInRoute={renderedInRoute} + id={id} + accentColor={accentColor} + title={title} + isBeta={isBeta} + /> + </View> + <RightMenus + renderedInRoute={renderedInRoute} + id={id} + headerRightButtons={headerRightButtons} + onPressDefaultRightButton={onPressDefaultRightButton} + search={hasSearch} + onSearch={onSearch} + /> + </> </View> </> ); }; -export const Header = React.memo(_Header, () => true); const styles = StyleSheet.create({ container: { diff --git a/apps/mobile/app/components/header/left-menus.tsx b/apps/mobile/app/components/header/left-menus.tsx index 5da228b8c..58d95ecf9 100644 --- a/apps/mobile/app/components/header/left-menus.tsx +++ b/apps/mobile/app/components/header/left-menus.tsx @@ -17,23 +17,29 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import { useThemeColors } from "@notesnook/theme"; import React from "react"; import { notesnook } from "../../../e2e/test.ids"; import { DDS } from "../../services/device-detection"; import Navigation from "../../services/navigation"; -import useNavigationStore from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; -import { useThemeColors } from "@notesnook/theme"; import { tabBarRef } from "../../utils/global-refs"; import { IconButton } from "../ui/icon-button"; -export const LeftMenus = () => { +export const LeftMenus = ({ + canGoBack, + onLeftButtonPress +}: { + canGoBack?: boolean; + onLeftButtonPress?: () => void; +}) => { const { colors } = useThemeColors(); const deviceMode = useSettingStore((state) => state.deviceMode); - const canGoBack = useNavigationStore((state) => state.canGoBack); const isTablet = deviceMode === "tablet"; - const onLeftButtonPress = () => { + const _onLeftButtonPress = () => { + if (onLeftButtonPress) return onLeftButtonPress(); + if (!canGoBack) { if (tabBarRef.current?.isDrawerOpen()) { Navigation.closeDrawer(); @@ -60,7 +66,7 @@ export const LeftMenus = () => { left={40} top={40} right={DDS.isLargeTablet() ? 10 : 10} - onPress={onLeftButtonPress} + onPress={_onLeftButtonPress} onLongPress={() => { Navigation.popToTop(); }} diff --git a/apps/mobile/app/components/header/right-menus.tsx b/apps/mobile/app/components/header/right-menus.tsx index a379a515b..afc6fb8dd 100644 --- a/apps/mobile/app/components/header/right-menus.tsx +++ b/apps/mobile/app/components/header/right-menus.tsx @@ -20,40 +20,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import React, { useRef } from "react"; import { Platform, StyleSheet, View } from "react-native"; //@ts-ignore +import { useThemeColors } from "@notesnook/theme"; import Menu from "react-native-reanimated-material-menu"; import { notesnook } from "../../../e2e/test.ids"; import Navigation from "../../services/navigation"; import SearchService from "../../services/search"; -import useNavigationStore from "../../stores/use-navigation-store"; +import { + HeaderRightButton, + RouteName +} from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; -import { useThemeColors } from "@notesnook/theme"; import { SIZE } from "../../utils/size"; import { sleep } from "../../utils/time"; import { Button } from "../ui/button"; import { IconButton } from "../ui/icon-button"; -export const RightMenus = () => { +export const RightMenus = ({ + headerRightButtons, + renderedInRoute, + id, + onPressDefaultRightButton, + search, + onSearch +}: { + headerRightButtons?: HeaderRightButton[]; + renderedInRoute: RouteName; + id?: string; + onPressDefaultRightButton?: () => void; + search?: boolean; + onSearch?: () => void; +}) => { const { colors } = useThemeColors(); const { colors: contextMenuColors } = useThemeColors("contextMenu"); const deviceMode = useSettingStore((state) => state.deviceMode); - const buttons = useNavigationStore((state) => state.headerRightButtons); - const currentScreen = useNavigationStore((state) => state.currentScreen.name); - const buttonAction = useNavigationStore((state) => state.buttonAction); const menuRef = useRef<Menu>(null); return ( <View style={styles.rightBtnContainer}> - {!currentScreen.startsWith("Settings") ? ( + {search ? ( <IconButton - onPress={async () => { - SearchService.prepareSearch(); - Navigation.navigate( - { - name: "Search" - }, - {} - ); - }} + onPress={onSearch} testID="icon-search" name="magnify" color={colors.primary.paragraph} @@ -63,9 +69,9 @@ export const RightMenus = () => { {deviceMode !== "mobile" ? ( <Button - onPress={buttonAction} + onPress={onPressDefaultRightButton} testID={notesnook.ids.default.addBtn} - icon={currentScreen === "Trash" ? "delete" : "plus"} + icon={renderedInRoute === "Trash" ? "delete" : "plus"} iconSize={SIZE.xl} type="shade" hitSlop={{ @@ -86,7 +92,7 @@ export const RightMenus = () => { /> ) : null} - {buttons && buttons.length > 0 ? ( + {headerRightButtons && headerRightButtons.length > 0 ? ( <Menu ref={menuRef} animationDuration={200} @@ -108,7 +114,7 @@ export const RightMenus = () => { /> } > - {buttons.map((item) => ( + {headerRightButtons.map((item) => ( <Button style={{ width: 150, diff --git a/apps/mobile/app/components/header/title.tsx b/apps/mobile/app/components/header/title.tsx index e0ee64041..989185e36 100644 --- a/apps/mobile/app/components/header/title.tsx +++ b/apps/mobile/app/components/header/title.tsx @@ -20,105 +20,74 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { useThemeColors } from "@notesnook/theme"; import React, { useCallback, useEffect, useState } from "react"; import { Platform } from "react-native"; -import { db } from "../../common/database"; -import NotebookScreen from "../../screens/notebook"; import { eSubscribeEvent, eUnSubscribeEvent } from "../../services/event-manager"; -import useNavigationStore from "../../stores/use-navigation-store"; import { eScrollEvent } from "../../utils/events"; import { SIZE } from "../../utils/size"; import Tag from "../ui/tag"; import Heading from "../ui/typography/heading"; -const titleState: { [id: string]: boolean } = {}; - -export const Title = () => { +export const Title = ({ + title, + isHiddenOnRender, + accentColor, + isBeta, + renderedInRoute, + id +}: { + title: string; + isHiddenOnRender?: boolean; + accentColor?: string; + isBeta?: boolean; + renderedInRoute: string; + id?: string; +}) => { const { colors } = useThemeColors(); - const currentScreen = useNavigationStore((state) => state.currentScreen); - const isNotebook = currentScreen.name === "Notebook"; - const isTopic = currentScreen?.name === "TopicNotes"; - const [hide, setHide] = useState( - isNotebook - ? typeof titleState[currentScreen.id as string] === "boolean" - ? titleState[currentScreen.id as string] - : true - : false - ); - const isHidden = titleState[currentScreen.id as string]; - const notebook = - isTopic && currentScreen.notebookId - ? db.notebooks?.notebook(currentScreen.notebookId)?.data - : null; - const title = currentScreen.title; - const isTag = currentScreen?.name === "TaggedNotes"; - + const [visible, setVisible] = useState(isHiddenOnRender); + const isTag = title.startsWith("#"); const onScroll = useCallback( - (data: { x: number; y: number }) => { - if (currentScreen.name !== "Notebook") { - setHide(false); - return; - } + (data: { x: number; y: number; id?: string; route: string }) => { + if (data.route !== renderedInRoute || data.id !== id) return; if (data.y > 150) { - if (!hide) return; - titleState[currentScreen.id as string] = false; - setHide(false); + if (!visible) return; + setVisible(false); } else { - if (hide) return; - titleState[currentScreen.id as string] = true; - setHide(true); + if (visible) return; + setVisible(true); } }, - [currentScreen.id, currentScreen.name, hide] + [id, renderedInRoute, visible] ); - useEffect(() => { - if (currentScreen.name === "Notebook") { - const value = - typeof titleState[currentScreen.id] === "boolean" - ? titleState[currentScreen.id] - : true; - setHide(value); - } else { - setHide(titleState[currentScreen.id as string]); - } - }, [currentScreen.id, currentScreen.name]); - useEffect(() => { eSubscribeEvent(eScrollEvent, onScroll); return () => { eUnSubscribeEvent(eScrollEvent, onScroll); }; - }, [hide, onScroll]); + }, [visible, onScroll]); - function navigateToNotebook() { - if (!isTopic) return; - if (notebook) { - NotebookScreen.navigate(notebook, true); - } - } return ( <> - {!hide && !isHidden ? ( + {!visible ? ( <Heading - onPress={navigateToNotebook} numberOfLines={1} size={SIZE.lg} style={{ flexWrap: "wrap", marginTop: Platform.OS === "ios" ? -1 : 0 }} - color={currentScreen.color || colors.primary.heading} + color={accentColor || colors.primary.heading} > {isTag ? ( <Heading size={SIZE.xl} color={colors.primary.accent}> # </Heading> ) : null} - {title}{" "} + {isTag ? title.slice(1) : title}{" "} <Tag - visible={currentScreen.beta} + visible={isBeta} text="BETA" style={{ backgroundColor: "transparent" diff --git a/apps/mobile/app/components/list-items/headers/section-header.tsx b/apps/mobile/app/components/list-items/headers/section-header.tsx index a243a0eea..b53e8d057 100644 --- a/apps/mobile/app/components/list-items/headers/section-header.tsx +++ b/apps/mobile/app/components/list-items/headers/section-header.tsx @@ -120,6 +120,7 @@ export const SectionHeader = React.memo< <> <Button onPress={() => { + console.log("Opening Sort sheet", screen, dataType); presentSheet({ component: <Sort screen={screen} type={dataType} /> }); @@ -169,7 +170,7 @@ export const SectionHeader = React.memo< SettingsService.set({ [dataType !== "notebook" ? "notesListMode" - : "notebooksListMode"]: isCompactModeEnabled + : "notebooksListMode"]: !isCompactModeEnabled ? "compact" : "normal" }); diff --git a/apps/mobile/app/components/list-items/note/index.tsx b/apps/mobile/app/components/list-items/note/index.tsx index c5852de52..9c135f3ed 100644 --- a/apps/mobile/app/components/list-items/note/index.tsx +++ b/apps/mobile/app/components/list-items/note/index.tsx @@ -105,7 +105,7 @@ const NoteItem = ({ {notebooks?.items ?.filter( (item) => - item.id !== useNavigationStore.getState().currentScreen?.id + item.id !== useNavigationStore.getState().currentRoute?.id ) .map((item) => ( <Button diff --git a/apps/mobile/app/components/list/empty.tsx b/apps/mobile/app/components/list/empty.tsx index 26f809568..9fe287c31 100644 --- a/apps/mobile/app/components/list/empty.tsx +++ b/apps/mobile/app/components/list/empty.tsx @@ -49,98 +49,96 @@ type EmptyListProps = { screen?: string; }; -export const Empty = React.memo( - function Empty({ - loading = true, - placeholder, - title, - color, - dataType, - screen - }: EmptyListProps) { - const { colors } = useThemeColors(); - const insets = useGlobalSafeAreaInsets(); - const { height } = useWindowDimensions(); - const introCompleted = useSettingStore( - (state) => state.settings.introCompleted - ); +export const Empty = React.memo(function Empty({ + loading = true, + placeholder, + title, + color, + dataType, + screen +}: EmptyListProps) { + const { colors } = useThemeColors(); + const insets = useGlobalSafeAreaInsets(); + const { height } = useWindowDimensions(); + const introCompleted = useSettingStore( + (state) => state.settings.introCompleted + ); - const tip = useTip( - screen === "Notes" && introCompleted - ? "first-note" - : placeholder?.type || ((dataType + "s") as any), - screen === "Notes" ? "notes" : "list" - ); + const tip = useTip( + screen === "Notes" && introCompleted + ? "first-note" + : placeholder?.type || ((dataType + "s") as any), + screen === "Notes" ? "notes" : "list" + ); - return ( - <View - style={[ - { - height: height - (140 + insets.top), - width: "80%", - justifyContent: "center", - alignSelf: "center" - } - ]} - > - {!loading ? ( - <> - <Tip - color={color ? color : "accent"} - tip={tip || ({ text: placeholder?.paragraph } as TTip)} + return ( + <View + style={[ + { + height: height - (140 + insets.top), + width: "80%", + justifyContent: "center", + alignSelf: "center" + } + ]} + > + {!loading ? ( + <> + <Tip + color={color} + tip={ + screen !== "Search" + ? tip || ({ text: placeholder?.paragraph } as TTip) + : ({ text: placeholder?.paragraph } as TTip) + } + style={{ + backgroundColor: "transparent", + paddingHorizontal: 0 + }} + /> + {placeholder?.button && ( + <Button + testID={notesnook.buttons.add} + type="grayAccent" + title={placeholder?.button} + iconPosition="right" + icon="arrow-right" + onPress={placeholder?.action} + buttonType={{ + text: color || colors.primary.accent + }} style={{ - backgroundColor: "transparent", - paddingHorizontal: 0 + alignSelf: "flex-start", + borderRadius: 5, + height: 40 }} /> - {placeholder?.button && ( - <Button - testID={notesnook.buttons.add} - type="grayAccent" - title={placeholder?.button} - iconPosition="right" - icon="arrow-right" - onPress={placeholder?.action} - buttonType={{ - text: color || colors.primary.accent - }} - style={{ - alignSelf: "flex-start", - borderRadius: 5, - height: 40 - }} - /> - )} - </> - ) : ( - <> - <View - style={{ - alignSelf: "center", - alignItems: "flex-start", - width: "100%" - }} - > - <Heading>{placeholder?.title}</Heading> - <Paragraph size={SIZE.sm} textBreakStrategy="balanced"> - {placeholder?.loading} - </Paragraph> - <Seperator /> - <ActivityIndicator - size={SIZE.lg} - color={color || colors.primary.accent} - /> - </View> - </> - )} - </View> - ); - }, - (prev, next) => { - if (prev.loading === next.loading) return true; - return false; - } -); + )} + </> + ) : ( + <> + <View + style={{ + alignSelf: "center", + alignItems: "flex-start", + width: "100%" + }} + > + <Heading>{placeholder?.title}</Heading> + <Paragraph size={SIZE.sm} textBreakStrategy="balanced"> + {placeholder?.loading} + </Paragraph> + <Seperator /> + <ActivityIndicator + size={SIZE.lg} + color={color || colors.primary.accent} + /> + </View> + </> + )} + </View> + ); +}); /** * Make a tips manager. diff --git a/apps/mobile/app/components/list/index.tsx b/apps/mobile/app/components/list/index.tsx index efee3a5e8..0563c3dcc 100644 --- a/apps/mobile/app/components/list/index.tsx +++ b/apps/mobile/app/components/list/index.tsx @@ -201,6 +201,7 @@ export default function List(props: ListProps) { dataType={props.dataType} color={props.customAccentColor} placeholder={props.placeholder} + screen={props.renderedInRoute} /> ) : null } diff --git a/apps/mobile/app/components/list/list-item.wrapper.tsx b/apps/mobile/app/components/list/list-item.wrapper.tsx index 600024ab7..4badb6a4f 100644 --- a/apps/mobile/app/components/list/list-item.wrapper.tsx +++ b/apps/mobile/app/components/list/list-item.wrapper.tsx @@ -251,7 +251,7 @@ async function resolveNotes(ids: string[]) { group.notebooks.map((id) => resolved.notebooks[id]) ), attachmentsCount: - (await db.attachments?.ofNote(noteId, "all"))?.length || 0 + (await db.attachments?.ofNote(noteId, "all").ids())?.length || 0 }; } return data; diff --git a/apps/mobile/app/components/properties/notebooks.js b/apps/mobile/app/components/properties/notebooks.js index e8d72b845..1d7c27f2d 100644 --- a/apps/mobile/app/components/properties/notebooks.js +++ b/apps/mobile/app/components/properties/notebooks.js @@ -17,39 +17,28 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import { useThemeColors } from "@notesnook/theme"; import React, { useEffect, useState } from "react"; -import { ScrollView, View } from "react-native"; +import { View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; import NotebookScreen from "../../screens/notebook"; -import { TopicNotes } from "../../screens/notes/topic-notes"; -import { - eSendEvent, - presentSheet, - ToastManager -} from "../../services/event-manager"; -import Navigation from "../../services/navigation"; -import { useNotebookStore } from "../../stores/use-notebook-store"; -import { useThemeColors } from "@notesnook/theme"; +import { eSendEvent, presentSheet } from "../../services/event-manager"; +import { eClearEditor } from "../../utils/events"; import { SIZE } from "../../utils/size"; import { Button } from "../ui/button"; import Heading from "../ui/typography/heading"; -import { eClearEditor } from "../../utils/events"; export default function Notebooks({ note, close, full }) { const { colors } = useThemeColors(); - const notebooks = useNotebookStore((state) => state.notebooks); async function getNotebooks(item) { let filteredNotebooks = []; const relations = await db.relations.to(note, "notebook").resolve(); - filteredNotebooks.push(relations); - if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks; return filteredNotebooks; } const [noteNotebooks, setNoteNotebooks] = useState([]); - useEffect(() => { getNotebooks().then((notebooks) => setNoteNotebooks(notebooks)); }); @@ -60,11 +49,6 @@ export default function Notebooks({ note, close, full }) { NotebookScreen.navigate(item, true); }; - const navigateTopic = (id, notebookId) => { - let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic; - if (!item) return; - TopicNotes.navigate(item, true); - }; const renderItem = (item) => ( <View key={item.id} @@ -105,56 +89,6 @@ export default function Notebooks({ note, close, full }) { > {item.title} </Heading> - - <ScrollView - horizontal={true} - showsHorizontalScrollIndicator={false} - style={{ - flexDirection: "row", - marginLeft: 8, - borderLeftColor: colors.primary.hover, - borderLeftWidth: 1, - paddingLeft: 8 - }} - > - {item.topics.map((topic) => ( - <Button - key={topic.id} - onPress={() => { - navigateTopic(topic.id, item.id); - eSendEvent(eClearEditor); - close(); - }} - onLongPress={async () => { - await db.notes.removeFromNotebook( - { - id: item.id, - topic: topic.id - }, - note - ); - useNotebookStore.getState().setNotebooks(); - Navigation.queueRoutesForUpdate(); - ToastManager.show({ - heading: "Note removed from topic", - context: "local", - type: "success" - }); - }} - title={topic.title} - type="gray" - height={30} - fontSize={SIZE.xs} - icon="bookmark-outline" - style={{ - marginRight: 5, - borderRadius: 100, - paddingHorizontal: 8 - }} - /> - ))} - <View style={{ width: 10 }} /> - </ScrollView> </View> ); diff --git a/apps/mobile/app/components/selection-header/index.js b/apps/mobile/app/components/selection-header/index.js index bc1bcad78..d9ffa9b03 100644 --- a/apps/mobile/app/components/selection-header/index.js +++ b/apps/mobile/app/components/selection-header/index.js @@ -46,8 +46,7 @@ export const SelectionHeader = React.memo(() => { ); const setSelectionMode = useSelectionStore((state) => state.setSelectionMode); const clearSelection = useSelectionStore((state) => state.clearSelection); - const currentScreen = useNavigationStore((state) => state.currentScreen); - const screen = currentScreen.name; + const currentRoute = useNavigationStore((state) => state.currentRoute); const insets = useGlobalSafeAreaInsets(); SearchService.prepareSearch?.(); const allItems = SearchService.getSearchInformation()?.get() || []; @@ -205,9 +204,9 @@ export const SelectionHeader = React.memo(() => { size={SIZE.xl} /> - {screen === "Trash" || - screen === "Notebooks" || - screen === "Reminders" ? null : ( + {currentRoute === "Trash" || + currentRoute === "Notebooks" || + currentRoute === "Reminders" ? null : ( <> <IconButton onPress={async () => { @@ -256,17 +255,15 @@ export const SelectionHeader = React.memo(() => { </> )} - {screen === "TopicNotes" || screen === "Notebook" ? ( + {currentRoute === "Notebook" ? ( <IconButton onPress={async () => { if (selectedItemsList.length > 0) { - const currentScreen = - useNavigationStore.getState().currentScreen; - - if (screen === "Notebook") { + const { focusedRouteId } = useNavigationStore.getState(); + if (currentRoute === "Notebook") { for (const item of selectedItemsList) { await db.relations.unlink( - { type: "notebook", id: currentScreen.id }, + { type: "notebook", id: focusedRouteId }, item ); } @@ -287,9 +284,7 @@ export const SelectionHeader = React.memo(() => { customStyle={{ marginLeft: 10 }} - tooltipText={`Remove from ${ - screen === "Notebook" ? "notebook" : "topic" - }`} + tooltipText={`Remove from Notebook`} tooltipPosition={4} testID="select-minus" color={colors.primary.paragraph} @@ -298,7 +293,7 @@ export const SelectionHeader = React.memo(() => { /> ) : null} - {screen === "Favorites" ? ( + {currentRoute === "Favorites" ? ( <IconButton onPress={addToFavorite} customStyle={{ @@ -312,7 +307,7 @@ export const SelectionHeader = React.memo(() => { /> ) : null} - {screen === "Trash" ? null : ( + {currentRoute === "Trash" ? null : ( <IconButton customStyle={{ marginLeft: 10 @@ -328,7 +323,7 @@ export const SelectionHeader = React.memo(() => { /> )} - {screen === "Trash" ? ( + {currentRoute === "Trash" ? ( <> <IconButton customStyle={{ diff --git a/apps/mobile/app/components/sheets/add-notebook/index.tsx b/apps/mobile/app/components/sheets/add-notebook/index.tsx index f6000a7d8..628a6c0ec 100644 --- a/apps/mobile/app/components/sheets/add-notebook/index.tsx +++ b/apps/mobile/app/components/sheets/add-notebook/index.tsx @@ -38,6 +38,7 @@ import Seperator from "../../ui/seperator"; import Heading from "../../ui/typography/heading"; import { MoveNotes } from "../move-notes/movenote"; import { eOnNotebookUpdated } from "../../../utils/events"; +import { getParentNotebookId } from "../../../utils/notebooks"; export const AddNotebookSheet = ({ notebook, @@ -84,9 +85,12 @@ export const AddNotebookSheet = ({ useMenuStore.getState().setMenuPins(); Navigation.queueRoutesForUpdate(); useRelationStore.getState().update(); - eSendEvent(eOnNotebookUpdated, parentNotebook?.id); if (notebook) { - eSendEvent(eOnNotebookUpdated, notebook.id); + const parent = await getParentNotebookId(notebook.id); + eSendEvent(eOnNotebookUpdated, parent); + setImmediate(() => { + eSendEvent(eOnNotebookUpdated, notebook.id); + }); } if (!notebook) { @@ -136,6 +140,11 @@ export const AddNotebookSheet = ({ onChangeText={(value) => { title.current = value; }} + onLayout={() => { + setImmediate(() => { + titleInput?.current?.focus(); + }); + }} placeholder="Enter a title" onSubmit={() => { descriptionInput.current?.focus(); @@ -160,8 +169,13 @@ export const AddNotebookSheet = ({ ); }; -AddNotebookSheet.present = (notebook?: Notebook, parentNotebook?: Notebook) => { +AddNotebookSheet.present = ( + notebook?: Notebook, + parentNotebook?: Notebook, + context?: string +) => { presentSheet({ + context: context, component: (ref, close) => ( <AddNotebookSheet notebook={notebook} diff --git a/apps/mobile/app/components/sheets/add-to/context.js b/apps/mobile/app/components/sheets/add-to/context.js deleted file mode 100644 index 550ecc00a..000000000 --- a/apps/mobile/app/components/sheets/add-to/context.js +++ /dev/null @@ -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); diff --git a/apps/mobile/app/components/sheets/add-to/filtered-list.js b/apps/mobile/app/components/sheets/add-to/filtered-list.js deleted file mode 100644 index a4be1c1fd..000000000 --- a/apps/mobile/app/components/sheets/add-to/filtered-list.js +++ /dev/null @@ -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" - /> - ); -}; diff --git a/apps/mobile/app/components/sheets/add-to/index.tsx b/apps/mobile/app/components/sheets/add-to/index.tsx index 2c5a81536..cbc90f5f5 100644 --- a/apps/mobile/app/components/sheets/add-to/index.tsx +++ b/apps/mobile/app/components/sheets/add-to/index.tsx @@ -17,12 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import { Note, Notebook } from "@notesnook/core"; +import { GroupHeader, Note } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; -import React, { RefObject, useCallback, useEffect, useMemo } from "react"; +import React, { RefObject, useCallback, useEffect } from "react"; import { Keyboard, TouchableOpacity, View } from "react-native"; -import { ActionSheetRef } from "react-native-actions-sheet"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; +import { ActionSheetRef, FlashList } from "react-native-actions-sheet"; import { db } from "../../../common/database"; import { eSendEvent, presentSheet } from "../../../services/event-manager"; import Navigation from "../../../services/navigation"; @@ -36,24 +35,53 @@ import { Dialog } from "../../dialog"; import DialogHeader from "../../dialog/dialog-header"; import { Button } from "../../ui/button"; import Paragraph from "../../ui/typography/paragraph"; -import { SelectionProvider } from "./context"; -import { FilteredList } from "./filtered-list"; -import { ListItem } from "./list-item"; -import { useItemSelectionStore } from "./store"; +import { NotebookItem } from "./notebook-item"; +import { useNotebookItemSelectionStore } from "./store"; +import SheetProvider from "../../sheet-provider"; +import { ItemSelection } from "../../../stores/item-selection-store"; + +async function updateInitialSelectionState(items: string[]) { + const relations = await db.relations + .to( + { + type: "note", + ids: items + }, + "notebook" + ) + .get(); + + const initialSelectionState: ItemSelection = {}; + const notebookIds = [ + ...new Set(relations.map((relation) => relation.fromId)) + ]; + + for (const id of notebookIds) { + const all = items.every((noteId) => { + return ( + relations.findIndex( + (relation) => relation.fromId === id && relation.toId === noteId + ) > -1 + ); + }); + if (all) { + initialSelectionState[id] = "selected"; + } else { + initialSelectionState[id] = "intermediate"; + } + } + useNotebookItemSelectionStore.setState({ + initialState: initialSelectionState, + selection: { ...initialSelectionState }, + multiSelect: relations.length > 1 + }); +} -/** - * Render all notebooks - * Render sub notebooks - * fix selection, remove topics stuff. - * show already selected notebooks regardless of their level - * show intermediate selection for nested notebooks at all levels. - * @returns - */ const MoveNoteSheet = ({ note, actionSheetRef }: { - note: Note; + note: Note | undefined; actionSheetRef: RefObject<ActionSheetRef>; }) => { const { colors } = useThemeColors(); @@ -63,72 +91,42 @@ const MoveNoteSheet = ({ (state) => state.selectedItemsList ); const setNotebooks = useNotebookStore((state) => state.setNotebooks); - - const multiSelect = useItemSelectionStore((state) => state.multiSelect); + const multiSelect = useNotebookItemSelectionStore( + (state) => state.multiSelect + ); useEffect(() => { + const items = note + ? [note.id] + : (selectedItemsList as Note[]).map((note) => note.id); + updateInitialSelectionState(items); return () => { - useItemSelectionStore.getState().setMultiSelect(false); - useItemSelectionStore.getState().setItemState({}); + useNotebookItemSelectionStore.setState({ + initialState: {}, + selection: {}, + multiSelect: false, + canEnableMultiSelectMode: true + }); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updateItemState = useCallback(function ( - item: Notebook, - state: "selected" | "intermediate" | "deselected" - ) { - const itemState = { ...useItemSelectionStore.getState().itemState }; - const mergeState = { - [item.id]: state - }; - useItemSelectionStore.getState().setItemState({ - ...itemState, - ...mergeState - }); - }, - []); - - const contextValue = useMemo( - () => ({ - toggleSelection: (item: Notebook) => { - const itemState = useItemSelectionStore.getState().itemState; - if (itemState[item.id] === "selected") { - updateItemState(item, "deselected"); - } else { - updateItemState(item, "selected"); - } - }, - deselect: (item: Notebook) => { - updateItemState(item, "deselected"); - }, - select: (item: Notebook) => { - updateItemState(item, "selected"); - }, - deselectAll: () => { - useItemSelectionStore.setState({ - itemState: {} - }); - } - }), - [updateItemState] - ); + }, [note, selectedItemsList]); const onSave = async () => { const noteIds = note ? [note.id] : selectedItemsList.map((n) => (n as Note).id); - const itemState = useItemSelectionStore.getState().itemState; - for (const id in itemState) { + + const changedNotebooks = useNotebookItemSelectionStore.getState().selection; + + for (const id in changedNotebooks) { const item = await db.notebooks.notebook(id); if (!item) continue; - if (itemState[id] === "selected") { - for (let noteId of noteIds) { - await db.relations.add(item, { id: noteId, type: "note" }); + if (changedNotebooks[id] === "selected") { + for (const id of noteIds) { + await db.relations.add(item, { id: id, type: "note" }); } - } else if (itemState[id] === "deselected") { - for (let noteId of noteIds) { - await db.relations.unlink(item, { id: noteId, type: "note" }); + } else if (changedNotebooks[id] === "deselected") { + for (const id of noteIds) { + await db.relations.unlink(item, { id: id, type: "note" }); } } } @@ -141,9 +139,18 @@ const MoveNoteSheet = ({ actionSheetRef.current?.hide(); }; + const renderNotebook = useCallback( + ({ item, index }: { item: string | GroupHeader; index: number }) => + (item as GroupHeader).type === "header" ? null : ( + <NotebookItem items={notebooks} id={item as string} index={index} /> + ), + [notebooks] + ); + return ( <> <Dialog context="move_note" /> + <SheetProvider context="link-notebooks" /> <View> <TouchableOpacity style={{ @@ -204,127 +211,50 @@ const MoveNoteSheet = ({ }} type="grayAccent" onPress={() => { - useItemSelectionStore.setState({ - itemState: {} - }); + const items = note + ? [note.id] + : (selectedItemsList as Note[]).map((note) => note.id); + updateInitialSelectionState(items); }} /> </View> - <SelectionProvider value={contextValue}> - <View + <View + style={{ + paddingHorizontal: 12, + maxHeight: dimensions.height * 0.85, + height: 50 * ((notebooks?.ids.length || 0) + 2) + }} + > + <FlashList + data={notebooks?.ids?.filter((id) => typeof id === "string")} style={{ - paddingHorizontal: 12, - maxHeight: dimensions.height * 0.85, - height: 50 * ((notebooks?.ids.length || 0) + 2) + width: "100%" }} - > - <FilteredList - ListEmptyComponent={ - notebooks?.ids.length ? null : ( - <View - style={{ - width: "100%", - height: "100%", - justifyContent: "center", - alignItems: "center" - }} - > - <Icon - name="book-outline" - color={colors.primary.icon} - size={100} - /> - <Paragraph style={{ marginBottom: 10 }}> - You do not have any notebooks. - </Paragraph> - </View> - ) - } - estimatedItemSize={50} - data={notebooks?.ids.length} - hasHeaderSearch={true} - renderItem={({ item, index }) => ( - <ListItem - item={item} - key={item.id} - index={index} - hasNotes={getSelectedNotesCountInItem(item) > 0} - sheetRef={actionSheetRef} - infoText={ - <> - {item.topics.length === 1 - ? item.topics.length + " topic" - : item.topics.length + " topics"} - </> - } - getListItems={getItemsForItem} - getSublistItemProps={(topic) => ({ - hasNotes: getSelectedNotesCountInItem(topic) > 0, - style: { - marginBottom: 0, - height: 40 - }, - onPress: (item) => { - const itemState = - useItemSelectionStore.getState().itemState; - const currentState = itemState[item.id]; - if (currentState !== "selected") { - resetItemState("deselected"); - contextValue.select(item); - } else { - contextValue.deselect(item); - } - }, - key: item.id, - type: "transparent" - })} - icon={(expanded) => ({ - name: expanded ? "chevron-up" : "chevron-down", - color: expanded - ? colors.primary.accent - : colors.primary.paragraph - })} - onScrollEnd={() => { - actionSheetRef.current?.handleChildScrollEnd(); - }} - hasSubList={true} - hasHeaderSearch={false} - type="grayBg" - sublistItemType="topic" - onAddItem={(title) => { - return onAddTopic(title, item); - }} - onAddSublistItem={(item) => { - openAddTopicDialog(item); - }} - onPress={(item) => { - const itemState = - useItemSelectionStore.getState().itemState; - const currentState = itemState[item.id]; - if (currentState !== "selected") { - resetItemState("deselected"); - contextValue.select(item); - } else { - contextValue.deselect(item); - } - }} - /> - )} - itemType="notebook" - onAddItem={async (title) => { - return await onAddNotebook(title); - }} - ListFooterComponent={<View style={{ height: 20 }} />} - /> - </View> - </SelectionProvider> + estimatedItemSize={50} + keyExtractor={(item) => item as string} + renderItem={renderNotebook} + ListEmptyComponent={ + <View + style={{ + flex: 1, + justifyContent: "center", + alignItems: "center", + height: 200 + }} + > + <Paragraph color={colors.primary.icon}>No notebooks</Paragraph> + </View> + } + ListFooterComponent={<View style={{ height: 50 }} />} + /> + </View> </View> </> ); }; -MoveNoteSheet.present = (note) => { +MoveNoteSheet.present = (note?: Note) => { presentSheet({ component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />, enableGesturesInScrollView: false, diff --git a/apps/mobile/app/components/sheets/add-to/list-header-item.js b/apps/mobile/app/components/sheets/add-to/list-header-item.js deleted file mode 100644 index e5260fa93..000000000 --- a/apps/mobile/app/components/sheets/add-to/list-header-item.js +++ /dev/null @@ -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> - ); -}; diff --git a/apps/mobile/app/components/sheets/add-to/list-item.js b/apps/mobile/app/components/sheets/add-to/list-item.js deleted file mode 100644 index 157c71a05..000000000 --- a/apps/mobile/app/components/sheets/add-to/list-item.js +++ /dev/null @@ -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> - ); -}; diff --git a/apps/mobile/app/components/sheets/add-to/notebook-item.tsx b/apps/mobile/app/components/sheets/add-to/notebook-item.tsx new file mode 100644 index 000000000..d0548aeb2 --- /dev/null +++ b/apps/mobile/app/components/sheets/add-to/notebook-item.tsx @@ -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> + ); +}; diff --git a/apps/mobile/app/components/sheets/add-to/store.ts b/apps/mobile/app/components/sheets/add-to/store.ts index 827f47086..729afcbb7 100644 --- a/apps/mobile/app/components/sheets/add-to/store.ts +++ b/apps/mobile/app/components/sheets/add-to/store.ts @@ -16,27 +16,24 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import create, { State } from "zustand"; +import create from "zustand"; +import { createItemSelectionStore } from "../../../stores/item-selection-store"; -type SelectionItemState = Record< - string, - "intermediate" | "selected" | "deselected" ->; - -export interface SelectionStore extends State { - itemState: SelectionItemState; - setItemState: (state: SelectionItemState) => void; - multiSelect: boolean; - setMultiSelect: (multiSelect: boolean) => void; -} - -export const useItemSelectionStore = create<SelectionStore>((set) => ({ - itemState: {}, - setItemState: (itemState) => { +export const useNotebookExpandedStore = create<{ + expanded: { + [id: string]: boolean; + }; + setExpanded: (id: string) => void; +}>((set, get) => ({ + expanded: {}, + setExpanded(id: string) { set({ - itemState + expanded: { + ...get().expanded, + [id]: !get().expanded[id] + } }); - }, - multiSelect: false, - setMultiSelect: (multiSelect) => set({ multiSelect }) + } })); + +export const useNotebookItemSelectionStore = createItemSelectionStore(true); diff --git a/apps/mobile/app/components/sheets/manage-tags/index.tsx b/apps/mobile/app/components/sheets/manage-tags/index.tsx index bd923c59e..e6b48377d 100644 --- a/apps/mobile/app/components/sheets/manage-tags/index.tsx +++ b/apps/mobile/app/components/sheets/manage-tags/index.tsx @@ -17,8 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import { VirtualizedGrouping } from "@notesnook/core"; import { Tags } from "@notesnook/core/dist/collections/tags"; -import { Note, Tag, isGroupHeader } from "@notesnook/core/dist/types"; +import { Note, Tag } from "@notesnook/core/dist/types"; import { useThemeColors } from "@notesnook/theme"; import React, { RefObject, @@ -28,12 +29,21 @@ import React, { useRef, useState } from "react"; -import { TextInput, View } from "react-native"; -import { ActionSheetRef, ScrollView } from "react-native-actions-sheet"; +import { TextInput, View, useWindowDimensions } from "react-native"; +import { + ActionSheetRef, + FlashList, + FlatList +} from "react-native-actions-sheet"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../../common/database"; +import { useDBItem } from "../../../hooks/use-db-item"; import { ToastManager, presentSheet } from "../../../services/event-manager"; import Navigation from "../../../services/navigation"; +import { + ItemSelection, + createItemSelectionStore +} from "../../../stores/item-selection-store"; import { useRelationStore } from "../../../stores/use-relation-store"; import { useTagStore } from "../../../stores/use-tag-store"; import { SIZE } from "../../../utils/size"; @@ -42,17 +52,40 @@ import Input from "../../ui/input"; import { PressableButton } from "../../ui/pressable"; import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; -import { VirtualizedGrouping } from "@notesnook/core"; -function tagHasSomeNotes(tagId: string, noteIds: string[]) { - return db.relations.from({ type: "tag", id: tagId }, "note").has(...noteIds); +async function updateInitialSelectionState(items: string[]) { + const relations = await db.relations + .to( + { + type: "note", + ids: items + }, + "tag" + ) + .get(); + + const initialSelectionState: ItemSelection = {}; + const tagId = [...new Set(relations.map((relation) => relation.fromId))]; + + for (const id of tagId) { + const all = items.every((noteId) => { + return ( + relations.findIndex( + (relation) => relation.fromId === id && relation.toId === noteId + ) > -1 + ); + }); + if (all) { + initialSelectionState[id] = "selected"; + } else { + initialSelectionState[id] = "intermediate"; + } + } + + return initialSelectionState; } -function tagHasAllNotes(tagId: string, noteIds: string[]) { - return db.relations - .from({ type: "tag", id: tagId }, "note") - .hasAll(...noteIds); -} +const useTagItemSelection = createItemSelectionStore(true); const ManageTagsSheet = (props: { notes?: Note[]; @@ -60,12 +93,44 @@ const ManageTagsSheet = (props: { }) => { const { colors } = useThemeColors(); const notes = useMemo(() => props.notes || [], [props.notes]); - const tags = useTagStore((state) => state.tags); - + const [tags, setTags] = useState<VirtualizedGrouping<Tag>>(); const [query, setQuery] = useState<string>(); const inputRef = useRef<TextInput>(null); const [focus, setFocus] = useState(false); const [queryExists, setQueryExists] = useState(false); + const dimensions = useWindowDimensions(); + const refreshSelection = useCallback(() => { + const ids = notes.map((item) => item.id); + updateInitialSelectionState(ids).then((selection) => { + useTagItemSelection.setState({ + initialState: selection, + selection: { ...selection } + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [notes, tags]); + + const refreshTags = useCallback(() => { + if (query && query.trim() !== "") { + db.lookup.tags(query).then((items) => { + setTags(items); + console.log("searched tags"); + }); + } else { + db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => { + console.log("items loaded tags"); + setTags(items); + }); + } + }, [query]); + + useEffect(() => { + refreshTags(); + }, [refreshTags, query]); + + useEffect(() => { + refreshSelection(); + }, [refreshSelection]); const checkQueryExists = (query: string) => { db.tags.all @@ -114,6 +179,7 @@ const ManageTagsSheet = (props: { useRelationStore.getState().update(); useTagStore.getState().setTags(); + refreshTags(); } catch (e) { ToastManager.show({ heading: "Cannot add tag", @@ -126,13 +192,64 @@ const ManageTagsSheet = (props: { Navigation.queueRoutesForUpdate(); }; + const onPress = useCallback( + async (id: string) => { + for (const note of notes) { + try { + if (!id) return; + const isSelected = + useTagItemSelection.getState().initialState[id] === "selected"; + if (isSelected) { + await db.relations.unlink( + { + id: id, + type: "tag" + }, + note + ); + } else { + await db.relations.add( + { + id: id, + type: "tag" + }, + note + ); + } + } catch (e) { + console.error(e); + } + } + useTagStore.getState().setTags(); + useRelationStore.getState().update(); + refreshTags(); + setTimeout(() => { + Navigation.queueRoutesForUpdate(); + }, 1); + refreshSelection(); + }, + [notes, refreshSelection, refreshTags] + ); + + const renderTag = useCallback( + ({ item }: { item: string; index: number }) => ( + <TagItem + key={item as string} + tags={tags as VirtualizedGrouping<Tag>} + id={item as string} + onPress={onPress} + /> + ), + [onPress, tags] + ); + return ( <View style={{ width: "100%", alignSelf: "center", paddingHorizontal: 12, - minHeight: focus ? "100%" : "60%" + maxHeight: dimensions.height * 0.85 }} > <Input @@ -161,31 +278,35 @@ const ManageTagsSheet = (props: { placeholder="Search or add a tag" /> - <ScrollView - overScrollMode="never" - scrollToOverflowEnabled={false} - keyboardDismissMode="none" - keyboardShouldPersistTaps="always" - > - {query && !queryExists ? ( - <PressableButton - key={"query_item"} - customStyle={{ - flexDirection: "row", - marginVertical: 5, - justifyContent: "space-between", - padding: 12 - }} - onPress={onSubmit} - type="selected" - > - <Heading size={SIZE.sm} color={colors.selected.heading}> - Add {'"' + "#" + query + '"'} - </Heading> - <Icon name="plus" color={colors.selected.icon} size={SIZE.lg} /> - </PressableButton> - ) : null} - {!tags || tags.ids.length === 0 ? ( + {query && !queryExists ? ( + <PressableButton + key={"query_item"} + customStyle={{ + flexDirection: "row", + marginVertical: 5, + justifyContent: "space-between", + padding: 12 + }} + onPress={onSubmit} + type="selected" + > + <Heading size={SIZE.sm} color={colors.selected.heading}> + Add {'"' + "#" + query + '"'} + </Heading> + <Icon name="plus" color={colors.selected.icon} size={SIZE.lg} /> + </PressableButton> + ) : null} + + <FlatList + data={tags?.ids?.filter((id) => typeof id === "string") as string[]} + style={{ + width: "100%" + }} + keyboardShouldPersistTaps + keyboardDismissMode="interactive" + keyExtractor={(item) => item as string} + renderItem={renderTag} + ListEmptyComponent={ <View style={{ width: "100%", @@ -204,19 +325,9 @@ const ManageTagsSheet = (props: { You do not have any tags. </Paragraph> </View> - ) : null} - - {tags?.ids - .filter((id) => !isGroupHeader(id)) - .map((item) => ( - <TagItem - key={item as string} - tags={tags} - id={item as string} - notes={notes} - /> - ))} - </ScrollView> + } + ListFooterComponent={<View style={{ height: 50 }} />} + /> </View> ); }; @@ -233,69 +344,17 @@ export default ManageTagsSheet; const TagItem = ({ id, - notes, - tags + tags, + onPress }: { id: string; - notes: Note[]; tags: VirtualizedGrouping<Tag>; + onPress: (id: string) => void; }) => { const { colors } = useThemeColors(); - const [tag, setTag] = useState<Tag>(); - const [selection, setSelection] = useState({ - all: false, - some: false - }); - const update = useRelationStore((state) => state.updater); + const [tag] = useDBItem(id, "tag", tags); + const selection = useTagItemSelection((state) => state.selection[id]); - const refresh = useCallback(() => { - tags.item(id).then(async (tag) => { - if (tag?.id) { - setSelection({ - all: await tagHasAllNotes( - tag.id, - notes.map((note) => note.id) - ), - some: await tagHasSomeNotes( - tag.id, - notes.map((note) => note.id) - ) - }); - } - setTag(tag); - }); - }, [id, tags, notes]); - - if (tag?.id !== id) { - refresh(); - } - - useEffect(() => { - if (tag?.id === id) { - refresh(); - } - }, [id, refresh, tag?.id, update]); - - const onPress = async () => { - for (const note of notes) { - try { - if (!tag?.id) return; - if (selection.all) { - await db.relations.unlink(tag, note); - } else { - await db.relations.add(tag, note); - } - } catch (e) { - console.error(e); - } - } - useTagStore.getState().setTags(); - useRelationStore.getState().update(); - setTimeout(() => { - Navigation.queueRoutesForUpdate(); - }, 1); - refresh(); - }; return ( <PressableButton customStyle={{ @@ -304,34 +363,32 @@ const TagItem = ({ justifyContent: "flex-start", height: 40 }} - onPress={onPress} + onPress={() => onPress(id)} type="gray" > {!tag ? null : ( - <IconButton + <Icon size={22} - customStyle={{ - marginRight: 5, - width: 23, - height: 23 - }} - onPress={onPress} + onPress={() => onPress(id)} color={ - selection.some || selection.all + selection === "selected" || selection === "intermediate" ? colors.selected.icon : colors.primary.icon } + style={{ + marginRight: 6 + }} testID={ - selection.all + selection === "selected" ? "check-circle-outline" - : selection.some + : selection === "intermediate" ? "minus-circle-outline" : "checkbox-blank-circle-outline" } name={ - selection.all + selection === "selected" ? "check-circle-outline" - : selection.some + : selection === "intermediate" ? "minus-circle-outline" : "checkbox-blank-circle-outline" } @@ -344,7 +401,7 @@ const TagItem = ({ style={{ width: 200, height: 30, - backgroundColor: colors.secondary.background, + // backgroundColor: colors.secondary.background, borderRadius: 5 }} /> diff --git a/apps/mobile/app/components/sheets/notebook-sheet/index.tsx b/apps/mobile/app/components/sheets/notebook-sheet/index.tsx index 7b25af93e..a0010d712 100644 --- a/apps/mobile/app/components/sheets/notebook-sheet/index.tsx +++ b/apps/mobile/app/components/sheets/notebook-sheet/index.tsx @@ -52,6 +52,24 @@ import Paragraph from "../../ui/typography/paragraph"; import { AddNotebookSheet } from "../add-notebook"; import Sort from "../sort"; +const SelectionContext = createContext<{ + selection: Notebook[]; + enabled: boolean; + setEnabled: (value: boolean) => void; + toggleSelection: (item: Notebook) => void; +}>({ + selection: [], + enabled: false, + setEnabled: (_value: boolean) => {}, + toggleSelection: (_item: Notebook) => {} +}); +const useSelection = () => useContext(SelectionContext); + +type NotebookParentProp = { + parent?: NotebookParentProp; + item?: Notebook; +}; + type ConfigItem = { id: string; type: string }; class NotebookSheetConfig { static storageKey: "$$sp"; @@ -88,8 +106,10 @@ const useNotebookExpandedStore = create<{ export const NotebookSheet = () => { const [collapsed, setCollapsed] = useState(false); - const currentScreen = useNavigationStore((state) => state.currentScreen); - const canShow = currentScreen.name === "Notebook"; + const currentRoute = useNavigationStore((state) => state.currentRoute); + const focusedRouteId = useNavigationStore((state) => state.focusedRouteId); + + const canShow = currentRoute === "Notebook"; const [selection, setSelection] = useState<Notebook[]>([]); const [enabled, setEnabled] = useState(false); const { colors } = useThemeColors("sheet"); @@ -103,7 +123,7 @@ export const NotebookSheet = () => { nestedNotebooks: notebooks, nestedNotebookNotesCount: totalNotes, groupOptions - } = useNotebook(currentScreen.name === "Notebook" ? root : undefined); + } = useNotebook(currentRoute === "Notebook" ? root : undefined); const PLACEHOLDER_DATA = { heading: "Notebooks", @@ -158,8 +178,8 @@ export const NotebookSheet = () => { useEffect(() => { if (canShow) { setTimeout(async () => { - const id = currentScreen?.id; - const nextRoot = await findRootNotebookId(id); + if (!focusedRouteId) return; + const nextRoot = await findRootNotebookId(focusedRouteId); setRoot(nextRoot); if (nextRoot !== currentItem.current) { setSelection([]); @@ -183,7 +203,7 @@ export const NotebookSheet = () => { setEnabled(false); ref.current?.hide(); } - }, [canShow, currentScreen?.id, currentScreen.name, onRequestUpdate]); + }, [canShow, currentRoute, onRequestUpdate, focusedRouteId]); return ( <ActionSheet @@ -206,7 +226,7 @@ export const NotebookSheet = () => { NotebookSheetConfig.set( { type: "notebook", - id: currentScreen.id as string + id: focusedRouteId as string }, index ); @@ -392,24 +412,6 @@ export const NotebookSheet = () => { ); }; -const SelectionContext = createContext<{ - selection: Notebook[]; - enabled: boolean; - setEnabled: (value: boolean) => void; - toggleSelection: (item: Notebook) => void; -}>({ - selection: [], - enabled: false, - setEnabled: (_value: boolean) => {}, - toggleSelection: (_item: Notebook) => {} -}); -const useSelection = () => useContext(SelectionContext); - -type NotebookParentProp = { - parent?: NotebookParentProp; - item?: Notebook; -}; - const NotebookItem = ({ id, totalNotes, @@ -430,12 +432,12 @@ const NotebookItem = ({ nestedNotebooks, notebook: item } = useNotebook(id, items); - const screen = useNavigationStore((state) => state.currentScreen); + const isFocused = useNavigationStore((state) => state.focusedRouteId === id); const { colors } = useThemeColors("sheet"); const selection = useSelection(); const isSelected = selection.selection.findIndex((selected) => selected.id === item?.id) > -1; - const isFocused = screen.id === id; + const { fontScale } = useWindowDimensions(); const expanded = useNotebookExpandedStore((state) => state.expanded[id]); diff --git a/apps/mobile/app/components/sheets/sort/index.js b/apps/mobile/app/components/sheets/sort/index.js index f813387b1..7c2cd751c 100644 --- a/apps/mobile/app/components/sheets/sort/index.js +++ b/apps/mobile/app/components/sheets/sort/index.js @@ -36,8 +36,9 @@ const Sort = ({ type, screen }) => { db.settings.getGroupOptions(screen === "Notes" ? "home" : type + "s") ); const updateGroupOptions = async (_groupOptions) => { - await db.settings.setGroupOptions(type, _groupOptions); - + const groupType = screen === "Notes" ? "home" : type + "s"; + console.log("updateGroupOptions for group", groupType, "in", screen); + await db.settings.setGroupOptions(groupType, _groupOptions); setGroupOptions(_groupOptions); setTimeout(() => { if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen); diff --git a/apps/mobile/app/components/side-menu/color-section.tsx b/apps/mobile/app/components/side-menu/color-section.tsx index b4f6ef2e2..1c96cda5d 100644 --- a/apps/mobile/app/components/side-menu/color-section.tsx +++ b/apps/mobile/app/components/side-menu/color-section.tsx @@ -66,7 +66,7 @@ const ColorItem = React.memo( const onHeaderStateChange = useCallback( (state: any) => { setTimeout(() => { - let id = state.currentScreen?.id; + let id = state.focusedRouteId; if (id === item.id) { setHeaderTextState({ id: state.currentScreen.id }); } else { diff --git a/apps/mobile/app/components/side-menu/menu-item.js b/apps/mobile/app/components/side-menu/menu-item.js index 2ddf25fd3..dc5bca3ac 100644 --- a/apps/mobile/app/components/side-menu/menu-item.js +++ b/apps/mobile/app/components/side-menu/menu-item.js @@ -34,10 +34,9 @@ export const MenuItem = React.memo( function MenuItem({ item, index, testID, rightBtn }) { const { colors } = useThemeColors(); const [headerTextState, setHeaderTextState] = useState( - useNavigationStore.getState().currentScreen + useNavigationStore.getState().focusedRouteId ); - const screenId = item.name.toLowerCase() + "_navigation"; - let isFocused = headerTextState?.id === screenId; + let isFocused = headerTextState?.id === item.name; const primaryColors = isFocused ? colors.selected : colors.primary; const _onPress = () => { @@ -59,9 +58,9 @@ export const MenuItem = React.memo( const onHeaderStateChange = useCallback( (state) => { setTimeout(() => { - let id = state.currentScreen?.id; - if (id === screenId) { - setHeaderTextState({ id: state.currentScreen.id }); + let id = state.focusedRouteId; + if (id === item.name) { + setHeaderTextState({ id: state.focusedRouteId }); } else { if (headerTextState !== null) { setHeaderTextState(null); @@ -69,7 +68,7 @@ export const MenuItem = React.memo( } }, 300); }, - [headerTextState, screenId] + [headerTextState, item.name] ); useEffect(() => { diff --git a/apps/mobile/app/components/side-menu/pinned-section.tsx b/apps/mobile/app/components/side-menu/pinned-section.tsx index 9b596bfb8..6b2df225f 100644 --- a/apps/mobile/app/components/side-menu/pinned-section.tsx +++ b/apps/mobile/app/components/side-menu/pinned-section.tsx @@ -17,19 +17,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, { useEffect, useRef, useState } from "react"; +import { Notebook, Tag } from "@notesnook/core"; +import { useThemeColors } from "@notesnook/theme"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { FlatList, View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; +import { db } from "../../common/database"; import NotebookScreen from "../../screens/notebook"; import { TaggedNotes } from "../../screens/notes/tagged"; -import { TopicNotes } from "../../screens/notes/topic-notes"; import Navigation from "../../services/navigation"; import { useMenuStore } from "../../stores/use-menu-store"; import useNavigationStore from "../../stores/use-navigation-store"; import { useNoteStore } from "../../stores/use-notes-store"; -import { useThemeColors } from "@notesnook/theme"; -import { db } from "../../common/database"; -import { normalize, SIZE } from "../../utils/size"; +import { SIZE, normalize } from "../../utils/size"; import { Properties } from "../properties"; import { Button } from "../ui/button"; import { Notice } from "../ui/notice"; @@ -38,8 +38,6 @@ import Seperator from "../ui/seperator"; import SheetWrapper from "../ui/sheet"; import Heading from "../ui/typography/heading"; import Paragraph from "../ui/typography/paragraph"; -import { useCallback } from "react"; -import { Notebook, Tag } from "@notesnook/core"; export const TagsSection = React.memo( function TagsSection() { @@ -127,7 +125,7 @@ export const PinItem = React.memo( const onHeaderStateChange = useCallback( (state: any) => { setTimeout(() => { - const id = state.currentScreen?.id; + const id = state.focusedRouteId; if (id === item.id) { setHeaderTextState({ id diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index eb6486c79..2cdb411fe 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -452,7 +452,7 @@ export const useActions = ({ } async function showAttachments() { - AttachmentDialog.present(item); + AttachmentDialog.present(item as Note); } async function exportNote() { @@ -486,12 +486,10 @@ export const useActions = ({ }; async function removeNoteFromNotebook() { - const currentScreen = useNavigationStore.getState().currentScreen; - if (currentScreen.name !== "Notebook") return; - await db.relations.unlink( - { type: "notebook", id: currentScreen.id }, - item - ); + const { currentRoute, focusedRouteId } = useNavigationStore.getState(); + if (currentRoute !== "Notebook" || !focusedRouteId) return; + + await db.relations.unlink({ type: "notebook", id: focusedRouteId }, item); Navigation.queueRoutesForUpdate(); close(); } @@ -499,7 +497,7 @@ export const useActions = ({ function addTo() { clearSelection(); setSelectedItem(item); - MoveNoteSheet.present(item); + MoveNoteSheet.present(item as Note); } async function addToFavorites() { @@ -857,11 +855,13 @@ export const useActions = ({ } useEffect(() => { - const currentScreen = useNavigationStore.getState().currentScreen; - if (item.type !== "note" || currentScreen.name !== "Notebook") return; + const { currentRoute, focusedRouteId } = useNavigationStore.getState(); + if (item.type !== "note" || currentRoute !== "Notebook" || !focusedRouteId) + return; + !!db.relations .to(item, "notebook") - .selector.find((v) => v("id", "==", currentScreen.id)) + .selector.find((v) => v("id", "==", focusedRouteId)) .then((notebook) => { setNoteInCurrentNotebook(!!notebook); }); diff --git a/apps/mobile/app/hooks/use-db-item.ts b/apps/mobile/app/hooks/use-db-item.ts index 676cf9400..58c63c094 100644 --- a/apps/mobile/app/hooks/use-db-item.ts +++ b/apps/mobile/app/hooks/use-db-item.ts @@ -56,9 +56,12 @@ export const useDBItem = <T extends keyof ItemTypeKey>( const onUpdateItem = (itemId?: string) => { if (typeof itemId === "string" && itemId !== id) return; if (!id) { - setItem(undefined); + if (item) { + setItem(undefined); + } return; } + console.log("onUpdateItem", id, type); if (items) { items.item(id).then((item) => { @@ -82,7 +85,7 @@ export const useDBItem = <T extends keyof ItemTypeKey>( return () => { eUnSubscribeEvent(eDBItemUpdate, onUpdateItem); }; - }, [id, type]); + }, [id, type, items, item]); return [ item as ItemTypeKey[T], @@ -116,6 +119,7 @@ export const useTotalNotes = ( } setTotalNotesById(totalNotesById); }); + console.log("useTotalNotes.getTotalNotes"); }, [ids, type]); useEffect(() => { diff --git a/apps/mobile/app/hooks/use-navigation-focus.ts b/apps/mobile/app/hooks/use-navigation-focus.ts index 6dc482fd1..c346bc12f 100644 --- a/apps/mobile/app/hooks/use-navigation-focus.ts +++ b/apps/mobile/app/hooks/use-navigation-focus.ts @@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { RefObject, useCallback, useEffect, useRef, useState } from "react"; +import { useRoute } from "@react-navigation/core"; +import useNavigationStore, { RouteName } from "../stores/use-navigation-store"; type NavigationFocus = { onFocus?: (prev: RefObject<boolean>) => boolean; @@ -31,6 +33,7 @@ export const useNavigationFocus = ( navigation: NativeStackNavigationProp<Record<string, object | undefined>>, { onFocus, onBlur, delay, focusOnInit = true }: NavigationFocus ) => { + const route = useRoute(); const [isFocused, setFocused] = useState(focusOnInit); const prev = useRef(false); const isBlurred = useRef(false); @@ -39,6 +42,12 @@ export const useNavigationFocus = ( setTimeout( () => { const shouldFocus = onFocus ? onFocus(prev) : true; + + const routeName = route.name?.startsWith("Settings") + ? "Settings" + : route.name; + useNavigationStore.getState().update(routeName as RouteName); + if (shouldFocus) { setFocused(true); prev.current = true; diff --git a/apps/mobile/app/hooks/use-notebook.ts b/apps/mobile/app/hooks/use-notebook.ts index 4cc1a2757..b3166bcb9 100644 --- a/apps/mobile/app/hooks/use-notebook.ts +++ b/apps/mobile/app/hooks/use-notebook.ts @@ -39,17 +39,19 @@ export const useNotebook = ( const onRequestUpdate = React.useCallback(() => { if (!item || !id) { - console.log("unset notebook"); - setNotebooks(undefined); + if (notebooks) { + setNotebooks(undefined); + } return; } + console.log("useNotebook.onRequestUpdate", id); db.relations .from(item, "notebook") .selector.sorted(db.settings.getGroupOptions("notebooks")) .then((notebooks) => { setNotebooks(notebooks); }); - }, [item, id]); + }, [item, id, notebooks]); useEffect(() => { onRequestUpdate(); @@ -75,7 +77,7 @@ export const useNotebook = ( eUnSubscribeEvent("groupOptionsUpdate", onUpdate); eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate); }; - }, [onUpdate, onRequestUpdate, id]); + }, [onUpdate, onRequestUpdate, id, refresh]); return { notebook: item, diff --git a/apps/mobile/app/navigation/navigation-stack.js b/apps/mobile/app/navigation/navigation-stack.js index 8bef45a3c..9e0c6dc41 100644 --- a/apps/mobile/app/navigation/navigation-stack.js +++ b/apps/mobile/app/navigation/navigation-stack.js @@ -35,7 +35,6 @@ import Notebooks from "../screens/notebooks"; import { ColoredNotes } from "../screens/notes/colored"; import { Monographs } from "../screens/notes/monographs"; import { TaggedNotes } from "../screens/notes/tagged"; -import { TopicNotes } from "../screens/notes/topic-notes"; import Reminders from "../screens/reminders"; import { Search } from "../screens/search"; import Settings from "../screens/settings"; @@ -120,44 +119,14 @@ const _Tabs = () => { <NativeStack.Screen name="Welcome" component={IntroStackNavigator} /> <NativeStack.Screen name="Notes" component={Home} /> <NativeStack.Screen name="Notebooks" component={Notebooks} /> - <NativeStack.Screen - options={{ lazy: true }} - name="Favorites" - component={Favorites} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="Trash" - component={Trash} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="Tags" - component={Tags} - /> + <NativeStack.Screen name="Favorites" component={Favorites} /> + <NativeStack.Screen name="Trash" component={Trash} /> + <NativeStack.Screen name="Tags" component={Tags} /> <NativeStack.Screen name="Settings" component={Settings} /> + <NativeStack.Screen name="TaggedNotes" component={TaggedNotes} /> + <NativeStack.Screen name="ColoredNotes" component={ColoredNotes} /> + <NativeStack.Screen name="Reminders" component={Reminders} /> <NativeStack.Screen - options={{ lazy: true }} - name="TaggedNotes" - component={TaggedNotes} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="TopicNotes" - component={TopicNotes} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="ColoredNotes" - component={ColoredNotes} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="Reminders" - component={Reminders} - /> - <NativeStack.Screen - options={{ lazy: true }} name="Monographs" initialParams={{ item: { type: "monograph" }, @@ -166,16 +135,8 @@ const _Tabs = () => { }} component={Monographs} /> - <NativeStack.Screen - options={{ lazy: true }} - name="Notebook" - component={NotebookScreen} - /> - <NativeStack.Screen - options={{ lazy: true }} - name="Search" - component={Search} - /> + <NativeStack.Screen name="Notebook" component={NotebookScreen} /> + <NativeStack.Screen name="Search" component={Search} /> </NativeStack.Navigator> ); }; diff --git a/apps/mobile/app/screens/favorites/index.tsx b/apps/mobile/app/screens/favorites/index.tsx index 3dbbc2a3e..742fdfd29 100644 --- a/apps/mobile/app/screens/favorites/index.tsx +++ b/apps/mobile/app/screens/favorites/index.tsx @@ -28,6 +28,7 @@ import SettingsService from "../../services/settings"; import { useFavoriteStore } from "../../stores/use-favorite-store"; import useNavigationStore from "../../stores/use-navigation-store"; import { useNoteStore } from "../../stores/use-notes-store"; +import { Header } from "../../components/header"; const prepareSearch = () => { SearchService.update({ placeholder: "Search in favorites", @@ -50,9 +51,7 @@ export const Favorites = ({ route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name - }); + useNavigationStore.getState().setFocusedRouteId(route?.name); SearchService.prepareSearch = prepareSearch; return !prev?.current; }, @@ -61,23 +60,43 @@ export const Favorites = ({ }); return ( - <DelayLayout wait={loading}> - <List - data={favorites} - dataType="note" - onRefresh={() => { - setFavorites(); + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={false} + hasSearch={true} + id={route.name} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`, + type: "note", + title: route.name, + route: route.name, + ids: favorites?.ids.filter( + (id) => typeof id === "string" + ) as string[] + }); }} - renderedInRoute="Favorites" - loading={loading || !isFocused} - placeholder={{ - title: "Your favorites", - paragraph: "You have not added any notes to favorites yet.", - loading: "Loading your favorites" - }} - headerTitle="Favorites" /> - </DelayLayout> + <DelayLayout wait={loading}> + <List + data={favorites} + dataType="note" + onRefresh={() => { + setFavorites(); + }} + renderedInRoute="Favorites" + loading={loading || !isFocused} + placeholder={{ + title: "Your favorites", + paragraph: "You have not added any notes to favorites yet.", + loading: "Loading your favorites" + }} + headerTitle="Favorites" + /> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/home/index.tsx b/apps/mobile/app/screens/home/index.tsx index a6c4e4c68..f95f4dbd9 100755 --- a/apps/mobile/app/screens/home/index.tsx +++ b/apps/mobile/app/screens/home/index.tsx @@ -18,26 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React from "react"; -import { db } from "../../common/database"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import SettingsService from "../../services/settings"; -import useNavigationStore from "../../stores/use-navigation-store"; import { useNoteStore } from "../../stores/use-notes-store"; import { openEditor } from "../notes/common"; - -const prepareSearch = () => { - SearchService.update({ - placeholder: "Type a keyword to search in notes", - type: "notes", - title: "Notes", - get: () => db.notes?.all - }); -}; +import useNavigationStore from "../../stores/use-navigation-store"; export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { const notes = useNoteStore((state) => state.notes); @@ -48,11 +38,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name - }); - SearchService.prepareSearch = prepareSearch; - useNavigationStore.getState().setButtonAction(openEditor); + useNavigationStore.getState().setFocusedRouteId(route.name); return !prev?.current; }, onBlur: () => false, @@ -60,23 +46,41 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { }); return ( - <DelayLayout wait={loading} delay={500}> - <List - data={notes} - dataType="note" - renderedInRoute="Notes" - loading={loading || !isFocused} - headerTitle="Notes" - placeholder={{ - title: "Notes", - paragraph: "You have not added any notes yet.", - button: "Add your first note", - action: openEditor, - loading: "Loading your notes" + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={false} + hasSearch={true} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`, + type: "note", + title: route.name, + route: route.name + }); }} + id={route.name} + onPressDefaultRightButton={openEditor} /> - <FloatingButton title="Create a new note" onPress={openEditor} /> - </DelayLayout> + <DelayLayout wait={loading} delay={500}> + <List + data={notes} + dataType="note" + renderedInRoute={route.name} + loading={loading || !isFocused} + headerTitle={route.name} + placeholder={{ + title: route.name?.toLowerCase(), + paragraph: `You have not added any ${route.name.toLowerCase()} yet.`, + button: "Add your first note", + action: openEditor, + loading: "Loading your notes" + }} + /> + <FloatingButton title="Create a new note" onPress={openEditor} /> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/notebook/index.tsx b/apps/mobile/app/screens/notebook/index.tsx index a125b1299..367789ff5 100644 --- a/apps/mobile/app/screens/notebook/index.tsx +++ b/apps/mobile/app/screens/notebook/index.tsx @@ -21,6 +21,7 @@ import { Note, Notebook } from "@notesnook/core/dist/types"; import React, { useEffect, useRef, useState } from "react"; import { db } from "../../common/database"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; import { NotebookHeader } from "../../components/list-items/headers/notebook-header"; import { AddNotebookSheet } from "../../components/sheets/add-notebook"; @@ -30,7 +31,6 @@ import { eUnSubscribeEvent } from "../../services/event-manager"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import useNavigationStore, { NotebookScreenParams } from "../../stores/use-navigation-store"; @@ -46,7 +46,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { onFocus: () => { Navigation.routeNeedsUpdate(route.name, onRequestUpdate); syncWithNavigation(); - useNavigationStore.getState().setButtonAction(openEditor); return false; }, onBlur: () => { @@ -56,21 +55,12 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { }); const syncWithNavigation = React.useCallback(() => { - useNavigationStore.getState().update( - { - name: route.name, - title: params.current?.title, - id: params.current?.item?.id, - type: "notebook" - }, - params.current?.canGoBack - ); + useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id); setOnFirstSave({ type: "notebook", id: params.current.item.id }); - SearchService.prepareSearch = prepareSearch; - }, [route.name]); + }, []); const onRequestUpdate = React.useCallback( async (data?: NotebookScreenParams) => { @@ -111,44 +101,25 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { }; }, []); - const prepareSearch = () => { - // SearchService.update({ - // placeholder: `Search in "${params.current.title}"`, - // type: "notes", - // title: params.current.title, - // get: () => { - // const notebook = db.notebooks?.notebook( - // params?.current?.item?.id - // )?.data; - // if (!notebook) return []; - // const notes = db.relations?.from(notebook, "note") || []; - // const topicNotes = db.notebooks - // .notebook(notebook.id) - // ?.topics.all.map((topic: Topic) => { - // return db.notes?.topicReferences - // .get(topic.id) - // .map((id: string) => db.notes?.note(id)?.data); - // }) - // .flat() - // .filter( - // (topicNote) => - // notes.findIndex((note) => note?.id !== topicNote?.id) === -1 - // ) as Note[]; - // return [...notes, ...topicNotes]; - // } - // }); - }; - - const PLACEHOLDER_DATA = { - title: params.current.item?.title, - paragraph: "You have not added any notes yet.", - button: "Add your first note", - action: openEditor, - loading: "Loading notebook notes" - }; - return ( <> + <Header + renderedInRoute={route.name} + title={params.current.item?.title} + canGoBack={params?.current?.canGoBack} + hasSearch={true} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${params.current.item?.title}`, + type: "note", + title: params.current.item?.title, + route: route.name, + ids: notes?.ids.filter((id) => typeof id === "string") as string[] + }); + }} + id={params.current.item?.id} + onPressDefaultRightButton={openEditor} + /> <DelayLayout> <List data={notes} @@ -170,7 +141,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { } /> } - placeholder={PLACEHOLDER_DATA} + placeholder={{ + title: params.current.item?.title, + paragraph: "You have not added any notes yet.", + button: "Add your first note", + action: openEditor, + loading: "Loading notebook notes" + }} /> </DelayLayout> </> @@ -179,19 +156,11 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { NotebookScreen.navigate = (item: Notebook, canGoBack?: boolean) => { if (!item) return; - Navigation.navigate<"Notebook">( - { - title: item.title, - name: "Notebook", - id: item.id, - type: "notebook" - }, - { - title: item.title, - item: item, - canGoBack - } - ); + Navigation.navigate<"Notebook">("Notebook", { + title: item.title, + item: item, + canGoBack + }); }; export default NotebookScreen; diff --git a/apps/mobile/app/screens/notebooks/index.tsx b/apps/mobile/app/screens/notebooks/index.tsx index 130cf70ba..69527f3ae 100644 --- a/apps/mobile/app/screens/notebooks/index.tsx +++ b/apps/mobile/app/screens/notebooks/index.tsx @@ -19,32 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import React, { useEffect } from "react"; import { Config } from "react-native-config"; -import { db } from "../../common/database"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; import { AddNotebookSheet } from "../../components/sheets/add-notebook"; import { Walkthrough } from "../../components/walkthroughs"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import SettingsService from "../../services/settings"; import useNavigationStore from "../../stores/use-navigation-store"; import { useNotebookStore } from "../../stores/use-notebook-store"; -const onPressFloatingButton = () => { +const onButtonPress = () => { AddNotebookSheet.present(); }; -const prepareSearch = () => { - SearchService.update({ - placeholder: "Type a keyword to search in notebooks", - type: "notebooks", - title: "Notebooks", - get: () => db.notebooks?.all - }); -}; - export const Notebooks = ({ navigation, route @@ -56,12 +46,7 @@ export const Notebooks = ({ route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name - }); - SearchService.prepareSearch = prepareSearch; - useNavigationStore.getState().setButtonAction(onPressFloatingButton); - + useNavigationStore.getState().setFocusedRouteId(route.name); return !prev?.current; }, onBlur: () => false, @@ -79,29 +64,47 @@ export const Notebooks = ({ }, [notebooks]); return ( - <DelayLayout delay={1}> - <List - data={notebooks} - dataType="notebook" - renderedInRoute="Notebooks" - loading={!isFocused} - placeholder={{ - title: "Your notebooks", - paragraph: "You have not added any notebooks yet.", - button: "Add your first notebook", - action: onPressFloatingButton, - loading: "Loading your notebooks" + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={route.params?.canGoBack} + hasSearch={true} + id={route.name} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`, + type: "notebook", + title: route.name, + route: route.name + }); }} - headerTitle="Notebooks" + onPressDefaultRightButton={onButtonPress} /> - - {!notebooks || notebooks.ids.length === 0 || !isFocused ? null : ( - <FloatingButton - title="Create a new notebook" - onPress={onPressFloatingButton} + <DelayLayout delay={1}> + <List + data={notebooks} + dataType="notebook" + renderedInRoute="Notebooks" + loading={!isFocused} + placeholder={{ + title: "Your notebooks", + paragraph: "You have not added any notebooks yet.", + button: "Add your first notebook", + action: onButtonPress, + loading: "Loading your notebooks" + }} + headerTitle="Notebooks" /> - )} - </DelayLayout> + + {!notebooks || notebooks.ids.length === 0 || !isFocused ? null : ( + <FloatingButton + title="Create a new notebook" + onPress={onButtonPress} + /> + )} + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/notes/colored.tsx b/apps/mobile/app/screens/notes/colored.tsx index 68ca469b5..9a11d297d 100644 --- a/apps/mobile/app/screens/notes/colored.tsx +++ b/apps/mobile/app/screens/notes/colored.tsx @@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { Color } from "@notesnook/core/dist/types"; -import { groupArray } from "@notesnook/core/dist/utils/grouping"; import React from "react"; -import NotesPage, { PLACEHOLDER_DATA } from "."; +import NotesPage from "."; import { db } from "../../common/database"; import Navigation, { NavigationProps } from "../../services/navigation"; import { NotesScreenParams } from "../../stores/use-navigation-store"; -import { openEditor, toCamelCase } from "./common"; +import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common"; export const ColoredNotes = ({ navigation, route @@ -54,18 +53,9 @@ ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => { ColoredNotes.navigate = (item: Color, canGoBack: boolean) => { if (!item) return; - Navigation.navigate<"ColoredNotes">( - { - name: "ColoredNotes", - title: toCamelCase(item.title), - id: item.id, - type: "color", - color: item.title?.toLowerCase() - }, - { - item: item, - canGoBack, - title: toCamelCase(item.title) - } - ); + Navigation.navigate<"ColoredNotes">("ColoredNotes", { + item: item, + canGoBack, + title: toCamelCase(item.title) + }); }; diff --git a/apps/mobile/app/screens/notes/common.ts b/apps/mobile/app/screens/notes/common.ts index 327ddde88..0293a6596 100644 --- a/apps/mobile/app/screens/notes/common.ts +++ b/apps/mobile/app/screens/notes/common.ts @@ -29,6 +29,14 @@ import { openLinkInBrowser } from "../../utils/functions"; import { tabBarRef } from "../../utils/global-refs"; import { editorController, editorState } from "../editor/tiptap/utils"; +export const PLACEHOLDER_DATA = { + title: "Your notes", + paragraph: "You have not added any notes yet.", + button: "Add your first Note", + action: openEditor, + loading: "Loading your notes." +}; + export function toCamelCase(title: string) { if (!title) return ""; return title.slice(0, 1).toUpperCase() + title.slice(1); diff --git a/apps/mobile/app/screens/notes/index.tsx b/apps/mobile/app/screens/notes/index.tsx index 502cd1e52..5e4022abe 100644 --- a/apps/mobile/app/screens/notes/index.tsx +++ b/apps/mobile/app/screens/notes/index.tsx @@ -17,21 +17,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import { - Color, - GroupedItems, - Item, - Note, - Topic -} from "@notesnook/core/dist/types"; +import { VirtualizedGrouping } from "@notesnook/core"; +import { Color, Note } from "@notesnook/core/dist/types"; import React, { useEffect, useRef, useState } from "react"; -import { View } from "react-native"; -import { db } from "../../common/database"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; -import { IconButton } from "../../components/ui/icon-button"; -import Paragraph from "../../components/ui/typography/paragraph"; +import { PlaceholderData } from "../../components/list/empty"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { eSubscribeEvent, @@ -45,39 +38,11 @@ import useNavigationStore, { RouteName } from "../../stores/use-navigation-store"; import { useNoteStore } from "../../stores/use-notes-store"; -import { SIZE } from "../../utils/size"; - -import NotebookScreen from "../notebook/index"; -import { - openEditor, - openMonographsWebpage, - setOnFirstSave, - toCamelCase -} from "./common"; -import { PlaceholderData } from "../../components/list/empty"; -import { VirtualizedGrouping } from "@notesnook/core"; +import { setOnFirstSave } from "./common"; export const WARNING_DATA = { title: "Some notes in this topic are not synced" }; -export const PLACEHOLDER_DATA = { - title: "Your notes", - paragraph: "You have not added any notes yet.", - button: "Add your first Note", - action: openEditor, - loading: "Loading your notes." -}; - -export const MONOGRAPH_PLACEHOLDER_DATA = { - heading: "Your monographs", - paragraph: "You have not published any notes as monographs yet.", - button: "Learn more about monographs", - action: openMonographsWebpage, - loading: "Loading published notes.", - type: "monographs", - buttonIcon: "information-outline" -}; - export interface RouteProps<T extends RouteName> extends NavigationProps<T> { get: ( params: NotesScreenParams, @@ -93,7 +58,6 @@ export interface RouteProps<T extends RouteName> extends NavigationProps<T> { function getItemType(routeName: RouteName) { if (routeName === "TaggedNotes") return "tag"; if (routeName === "ColoredNotes") return "color"; - if (routeName === "TopicNotes") return "topic"; if (routeName === "Monographs") return "monograph"; return "note"; } @@ -110,24 +74,24 @@ const NotesPage = ({ "NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes" >) => { const params = useRef<NotesScreenParams>(route?.params); - const [notes, setNotes] = useState<VirtualizedGrouping<Note>>(); - const loading = useNoteStore((state) => state.loading); const [loadingNotes, setLoadingNotes] = useState(true); const isMonograph = route.name === "Monographs"; - - // const notebook = - // route.name === "TopicNotes" && - // params.current.item.type === "topic" && - // params.current.item.notebookId - // ? db.notebooks?.notebook((params.current.item as Topic).notebookId)?.data - // : null; + const title = + params.current?.item.type === "tag" + ? "#" + params.current?.item.title + : params.current?.item.title; + const accentColor = + route.name === "ColoredNotes" + ? (params.current?.item as Color)?.colorCode + : undefined; const isFocused = useNavigationFocus(navigation, { onFocus: (prev) => { Navigation.routeNeedsUpdate(route.name, onRequestUpdate); syncWithNavigation(); + if (focusControl) return !prev.current; return false; }, @@ -143,10 +107,7 @@ const NotesPage = ({ SearchService.update({ placeholder: `Search in ${item.title}`, type: "notes", - title: - item.type === "tag" - ? "#" + item.title - : toCamelCase((item as Color).title), + title: item.type === "tag" ? "#" + item.title : item.title, get: () => { return get(params.current, false); } @@ -154,31 +115,16 @@ const NotesPage = ({ }, [get]); const syncWithNavigation = React.useCallback(() => { - const { item, title } = params.current; - useNavigationStore.getState().update( - { - name: route.name, - title: - route.name === "ColoredNotes" ? toCamelCase(title as string) : title, - id: item?.id, - type: "notes", - notebookId: item.type === "topic" ? item.notebookId : undefined, - color: - item.type === "color" && route.name === "ColoredNotes" - ? item.title?.toLowerCase() - : undefined - }, - params.current.canGoBack, - rightButtons && rightButtons(params.current) - ); - SearchService.prepareSearch = prepareSearch; - useNavigationStore.getState().setButtonAction(onPressFloatingButton); + const { item } = params.current; + + useNavigationStore + .getState() + .setFocusedRouteId(params?.current?.item?.id || route.name); !isMonograph && setOnFirstSave({ type: getItemType(route.name), - id: item.id, - notebook: item.type === "topic" ? item.notebookId : undefined + id: item.id }); }, [ isMonograph, @@ -192,9 +138,6 @@ const NotesPage = ({ async (data?: NotesScreenParams) => { const isNew = data && data?.item?.id !== params.current?.item?.id; if (data) params.current = data; - params.current.title = - params.current.title || - (params.current.item as Item & { title: string }).title; const { item } = params.current; try { if (isNew) setLoadingNotes(true); @@ -204,9 +147,8 @@ const NotesPage = ({ )) as VirtualizedGrouping<Note>; if ( - ((item.type === "tag" || item.type === "color") && - (!notes || notes.ids.length === 0)) || - (item.type === "topic" && !notes) + (item.type === "tag" || item.type === "color") && + (!notes || notes.ids.length === 0) ) { return Navigation.goBack(); } @@ -243,81 +185,51 @@ const NotesPage = ({ }, [onRequestUpdate, route.name]); return ( - <DelayLayout - color={ - route.name === "ColoredNotes" - ? (params.current?.item as Color)?.colorCode - : undefined - } - wait={loading || loadingNotes} - > - {/* {route.name === "TopicNotes" ? ( - <View - style={{ - width: "100%", - paddingHorizontal: 12, - flexDirection: "row", - alignItems: "center" - }} - > - <Paragraph - onPress={() => { - Navigation.navigate({ - name: "Notebooks", - title: "Notebooks" - }); - }} - size={SIZE.xs} - > - Notebooks - </Paragraph> - {notebook ? ( - <> - <IconButton - name="chevron-right" - size={14} - customStyle={{ width: 25, height: 25 }} - /> - <Paragraph - onPress={() => { - NotebookScreen.navigate(notebook, true); - }} - size={SIZE.xs} - > - {notebook.title} - </Paragraph> - </> - ) : null} - </View> - ) : null} */} - <List - data={notes} - dataType="note" - onRefresh={onRequestUpdate} - loading={loading || !isFocused} - renderedInRoute="Notes" - headerTitle={params.current.title} - customAccentColor={ - route.name === "ColoredNotes" - ? (params.current?.item as Color)?.colorCode - : undefined + <> + <Header + renderedInRoute={route.name} + title={title} + canGoBack={params?.current?.canGoBack} + hasSearch={true} + id={ + route.name === "Monographs" ? "Monographs" : params?.current.item?.id } - placeholder={placeholder} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${title}`, + type: "note", + title: title, + route: route.name, + ids: notes?.ids?.filter((id) => typeof id === "string") as string[] + }); + }} + accentColor={accentColor} + onPressDefaultRightButton={onPressFloatingButton} + headerRightButtons={rightButtons?.(params?.current)} /> - {!isMonograph && - ((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? ( - <FloatingButton - color={ - route.name === "ColoredNotes" - ? (params.current?.item as Color)?.colorCode - : undefined - } - title="Create a note" - onPress={onPressFloatingButton} + <DelayLayout color={accentColor} wait={loading || loadingNotes}> + <List + data={notes} + dataType="note" + onRefresh={onRequestUpdate} + loading={loading || !isFocused} + renderedInRoute="Notes" + headerTitle={title} + customAccentColor={accentColor} + placeholder={placeholder} /> - ) : null} - </DelayLayout> + + {!isMonograph && + ((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? ( + <FloatingButton + color={accentColor} + title="Create a note" + onPress={onPressFloatingButton} + /> + ) : null} + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/notes/monographs.tsx b/apps/mobile/app/screens/notes/monographs.tsx index a7e61dcb6..6d3a88a77 100644 --- a/apps/mobile/app/screens/notes/monographs.tsx +++ b/apps/mobile/app/screens/notes/monographs.tsx @@ -18,12 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React from "react"; -import NotesPage, { PLACEHOLDER_DATA } from "."; +import NotesPage from "."; import { db } from "../../common/database"; import Navigation, { NavigationProps } from "../../services/navigation"; import { NotesScreenParams } from "../../stores/use-navigation-store"; -import { MonographType } from "../../utils/types"; import { openMonographsWebpage } from "./common"; + +export const MONOGRAPH_PLACEHOLDER_DATA = { + title: "Your monographs", + paragraph: "You have not published any notes as monographs yet.", + button: "Learn more about monographs", + action: openMonographsWebpage, + loading: "Loading published notes.", + type: "monograph", + buttonIcon: "information-outline" +}; + export const Monographs = ({ navigation, route @@ -33,7 +43,7 @@ export const Monographs = ({ navigation={navigation} route={route} get={Monographs.get} - placeholder={PLACEHOLDER_DATA} + placeholder={MONOGRAPH_PLACEHOLDER_DATA} onPressFloatingButton={openMonographsWebpage} canGoBack={route.params?.canGoBack} focusControl={true} @@ -49,16 +59,10 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => { return await db.monographs.all.grouped(db.settings.getGroupOptions("notes")); }; -Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => { - Navigation.navigate<"Monographs">( - { - name: "Monographs", - type: "monograph" - }, - { - item: { type: "monograph" } as any, - canGoBack: canGoBack as boolean, - title: "Monographs" - } - ); +Monographs.navigate = (canGoBack?: boolean) => { + Navigation.navigate<"Monographs">("Monographs", { + item: { type: "monograph" } as any, + canGoBack: canGoBack as boolean, + title: "Monographs" + }); }; diff --git a/apps/mobile/app/screens/notes/tagged.tsx b/apps/mobile/app/screens/notes/tagged.tsx index 0bff3085f..ee754df71 100644 --- a/apps/mobile/app/screens/notes/tagged.tsx +++ b/apps/mobile/app/screens/notes/tagged.tsx @@ -19,11 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { Tag } from "@notesnook/core/dist/types"; import React from "react"; -import NotesPage, { PLACEHOLDER_DATA } from "."; +import NotesPage from "."; import { db } from "../../common/database"; import Navigation, { NavigationProps } from "../../services/navigation"; import { NotesScreenParams } from "../../stores/use-navigation-store"; -import { openEditor } from "./common"; +import { PLACEHOLDER_DATA, openEditor } from "./common"; + export const TaggedNotes = ({ navigation, route @@ -53,17 +54,9 @@ TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => { TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => { if (!item) return; - Navigation.navigate<"TaggedNotes">( - { - name: "TaggedNotes", - title: item.title, - id: item.id, - type: "tag" - }, - { - item: item, - canGoBack, - title: item.title - } - ); + Navigation.navigate<"TaggedNotes">("TaggedNotes", { + item: item, + canGoBack, + title: item.title + }); }; diff --git a/apps/mobile/app/screens/notes/topic-notes.tsx b/apps/mobile/app/screens/notes/topic-notes.tsx deleted file mode 100644 index ca763f064..000000000 --- a/apps/mobile/app/screens/notes/topic-notes.tsx +++ /dev/null @@ -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 - } - ); -}; diff --git a/apps/mobile/app/screens/reminders/index.tsx b/apps/mobile/app/screens/reminders/index.tsx index a57c1f3a9..4fa28b081 100644 --- a/apps/mobile/app/screens/reminders/index.tsx +++ b/apps/mobile/app/screens/reminders/index.tsx @@ -18,37 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React from "react"; -import { db } from "../../common/database"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; import ReminderSheet from "../../components/sheets/reminder"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import SettingsService from "../../services/settings"; import useNavigationStore from "../../stores/use-navigation-store"; import { useReminderStore } from "../../stores/use-reminder-store"; -const prepareSearch = () => { - SearchService.update({ - placeholder: "Search in reminders", - type: "reminders", - title: "Reminders", - get: () => db.reminders?.all - }); -}; - -const PLACEHOLDER_DATA = { - title: "Your reminders", - paragraph: "You have not set any reminders yet.", - button: "Set a new reminder", - action: () => { - ReminderSheet.present(); - }, - loading: "Loading reminders" -}; - export const Reminders = ({ navigation, route @@ -60,13 +40,8 @@ export const Reminders = ({ route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name, - beta: true - }); - SearchService.prepareSearch = prepareSearch; - useNavigationStore.getState().setButtonAction(PLACEHOLDER_DATA.action); + useNavigationStore.getState().setFocusedRouteId(route.name); return !prev?.current; }, onBlur: () => false, @@ -74,23 +49,52 @@ export const Reminders = ({ }); return ( - <DelayLayout> - <List - data={reminders} - dataType="reminder" - headerTitle="Reminders" - renderedInRoute="Reminders" - loading={!isFocused} - placeholder={PLACEHOLDER_DATA} - /> - - <FloatingButton - title="Set a new reminder" - onPress={() => { + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={false} + hasSearch={true} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name}`, + type: "reminder", + title: route.name, + route: route.name + }); + }} + id={route.name} + onPressDefaultRightButton={() => { ReminderSheet.present(); }} /> - </DelayLayout> + + <DelayLayout> + <List + data={reminders} + dataType="reminder" + headerTitle="Reminders" + renderedInRoute="Reminders" + loading={!isFocused} + placeholder={{ + title: "Your reminders", + paragraph: "You have not set any reminders yet.", + button: "Set a new reminder", + action: () => { + ReminderSheet.present(); + }, + loading: "Loading reminders" + }} + /> + + <FloatingButton + title="Set a new reminder" + onPress={() => { + ReminderSheet.present(); + }} + /> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/search/index.js b/apps/mobile/app/screens/search/index.js deleted file mode 100644 index b26aea3e0..000000000 --- a/apps/mobile/app/screens/search/index.js +++ /dev/null @@ -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> - ); -}; diff --git a/apps/mobile/app/screens/search/index.tsx b/apps/mobile/app/screens/search/index.tsx new file mode 100644 index 000000000..83faeef3a --- /dev/null +++ b/apps/mobile/app/screens/search/index.tsx @@ -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..." + }} + /> + </> + ); +}; diff --git a/apps/mobile/app/screens/search/search-bar.js b/apps/mobile/app/screens/search/search-bar.js deleted file mode 100644 index 066153cb7..000000000 --- a/apps/mobile/app/screens/search/search-bar.js +++ /dev/null @@ -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> - ); -}; diff --git a/apps/mobile/app/screens/search/search-bar.tsx b/apps/mobile/app/screens/search/search-bar.tsx new file mode 100644 index 000000000..0243e4040 --- /dev/null +++ b/apps/mobile/app/screens/search/search-bar.tsx @@ -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> + ); +}; diff --git a/apps/mobile/app/screens/settings/editor/state.ts b/apps/mobile/app/screens/settings/editor/state.ts index b1c5912c5..2de429f18 100644 --- a/apps/mobile/app/screens/settings/editor/state.ts +++ b/apps/mobile/app/screens/settings/editor/state.ts @@ -71,7 +71,7 @@ export const useDragState = create<DragState>( presets["custom"] = _data; db.settings.setToolbarConfig( - useSettingStore.getState().deviceMode || "mobile", + useSettingStore.getState().deviceMode || ("mobile" as any), { preset: "custom", config: clone(_data) @@ -81,7 +81,7 @@ export const useDragState = create<DragState>( }, setPreset: (preset) => { db.settings.setToolbarConfig( - useSettingStore.getState().deviceMode || "mobile", + useSettingStore.getState().deviceMode || ("mobile" as any), { preset, config: preset === "custom" ? clone(get().customPresetData) : [] @@ -99,7 +99,7 @@ export const useDragState = create<DragState>( const user = await db.user?.getUser(); if (!user) return; const toolbarConfig = db.settings.getToolbarConfig( - useSettingStore.getState().deviceMode || "mobile" + useSettingStore.getState().deviceMode || ("mobile" as any) ); if (!toolbarConfig) { logger.info("DragState", "No user defined toolbar config was found"); @@ -110,25 +110,20 @@ export const useDragState = create<DragState>( preset: preset, data: preset === "custom" - ? clone(toolbarConfig?.config) + ? clone(toolbarConfig?.config as any[]) : clone(presets[preset]), customPresetData: preset === "custom" - ? clone(toolbarConfig?.config) + ? clone(toolbarConfig?.config as any[]) : clone(presets["custom"]) }); } }), { name: "drag-state-storage", // unique name - getStorage: () => MMKV as StateStorage, + getStorage: () => MMKV as unknown as StateStorage, onRehydrateStorage: () => { return () => { - logger.info( - "DragState", - "rehydrated drag state", - useNoteStore.getState().loading - ); if (!useNoteStore.getState().loading) { useDragState.getState().init(); } else { diff --git a/apps/mobile/app/screens/settings/group.tsx b/apps/mobile/app/screens/settings/group.tsx index be613040e..b73fdb4eb 100644 --- a/apps/mobile/app/screens/settings/group.tsx +++ b/apps/mobile/app/screens/settings/group.tsx @@ -29,6 +29,7 @@ import { tabBarRef } from "../../utils/global-refs"; import { components } from "./components"; import { SectionItem } from "./section-item"; import { RouteParams, SettingSection } from "./types"; +import { Header } from "../../components/header"; const keyExtractor = (item: SettingSection) => item.id; const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent( @@ -42,13 +43,7 @@ const Group = ({ useNavigationFocus(navigation, { onFocus: () => { tabBarRef.current?.lock(); - useNavigationStore.getState().update( - { - name: "SettingsGroup", - title: route.params.name as string - }, - true - ); + useNavigationStore.getState().setFocusedRouteId("Settings"); return false; } }); @@ -62,25 +57,33 @@ const Group = ({ ); return ( - <DelayLayout type="settings" delay={1}> - <View - style={{ - flex: 1 - }} - > - {route.params.sections ? ( - <AnimatedKeyboardAvoidingFlatList - entering={FadeInDown} - data={route.params.sections} - keyExtractor={keyExtractor} - renderItem={renderItem} - enableOnAndroid - enableAutomaticScroll - /> - ) : null} - {route.params.component ? components[route.params.component] : null} - </View> - </DelayLayout> + <> + <Header + renderedInRoute="Settings" + title={route.params.name as string} + canGoBack={true} + id="Settings" + /> + <DelayLayout type="settings" delay={1}> + <View + style={{ + flex: 1 + }} + > + {route.params.sections ? ( + <AnimatedKeyboardAvoidingFlatList + entering={FadeInDown} + data={route.params.sections} + keyExtractor={keyExtractor} + renderItem={renderItem} + enableOnAndroid + enableAutomaticScroll + /> + ) : null} + {route.params.component ? components[route.params.component] : null} + </View> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/settings/home.tsx b/apps/mobile/app/screens/settings/home.tsx index 098425c6f..511b6487c 100644 --- a/apps/mobile/app/screens/settings/home.tsx +++ b/apps/mobile/app/screens/settings/home.tsx @@ -38,6 +38,7 @@ import { SectionGroup } from "./section-group"; import { settingsGroups } from "./settings-data"; import { RouteParams, SettingSection } from "./types"; import SettingsUserSection from "./user-section"; +import { Header } from "../../components/header"; const keyExtractor = (item: SettingSection) => item.id; const Home = ({ @@ -48,9 +49,7 @@ const Home = ({ useNavigationFocus(navigation, { onFocus: () => { - useNavigationStore.getState().update({ - name: "Settings" - }); + useNavigationStore.getState().setFocusedRouteId("Settings"); return false; }, focusOnInit: true @@ -74,19 +73,17 @@ const Home = ({ }, []); return ( - <DelayLayout delay={300} type="settings"> - {loading && ( - //@ts-ignore // Migrate to typescript required. - <BaseDialog animated={false} bounce={false} visible={true}> - <View - style={{ - width: "100%", - height: "100%", - backgroundColor: colors.primary.background, - justifyContent: "center", - alignItems: "center" - }} - > + <> + <Header + renderedInRoute="Settings" + title="Settings" + canGoBack={false} + id="Settings" + /> + <DelayLayout delay={300} type="settings"> + {loading && ( + //@ts-ignore // Migrate to typescript required. + <BaseDialog animated={false} bounce={false} visible={true}> <View style={{ width: "100%", @@ -96,45 +93,55 @@ const Home = ({ alignItems: "center" }} > - <Heading color={colors.primary.paragraph} size={SIZE.lg}> - Logging out - </Heading> - <Paragraph color={colors.secondary.paragraph}> - Please wait while we log out and clear app data. - </Paragraph> <View style={{ - flexDirection: "row", - width: 100, - marginTop: 15 + width: "100%", + height: "100%", + backgroundColor: colors.primary.background, + justifyContent: "center", + alignItems: "center" }} > - <ProgressBarComponent - height={5} - width={100} - animated={true} - useNativeDriver - indeterminate - indeterminateAnimationDuration={2000} - unfilledColor={colors.secondary.background} - color={colors.primary.accent} - borderWidth={0} - /> + <Heading color={colors.primary.paragraph} size={SIZE.lg}> + Logging out + </Heading> + <Paragraph color={colors.secondary.paragraph}> + Please wait while we log out and clear app data. + </Paragraph> + <View + style={{ + flexDirection: "row", + width: 100, + marginTop: 15 + }} + > + <ProgressBarComponent + height={5} + width={100} + animated={true} + useNativeDriver + indeterminate + indeterminateAnimationDuration={2000} + unfilledColor={colors.secondary.background} + color={colors.primary.accent} + borderWidth={0} + /> + </View> </View> </View> - </View> - </BaseDialog> - )} + </BaseDialog> + )} - <Animated.FlatList - entering={FadeInDown} - data={settingsGroups} - windowSize={1} - keyExtractor={keyExtractor} - ListFooterComponent={<View style={{ height: 200 }} />} - renderItem={renderItem} - /> - </DelayLayout> + <Animated.FlatList + entering={FadeInDown} + data={settingsGroups} + windowSize={1} + keyExtractor={keyExtractor} + ListFooterComponent={<View style={{ height: 200 }} />} + renderItem={renderItem} + /> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/settings/index.tsx b/apps/mobile/app/screens/settings/index.tsx index debe48d24..c9d62393c 100644 --- a/apps/mobile/app/screens/settings/index.tsx +++ b/apps/mobile/app/screens/settings/index.tsx @@ -26,34 +26,6 @@ import Home from "./home"; import { RouteParams } from "./types"; const SettingsStack = createNativeStackNavigator<RouteParams>(); -// const Home = React.lazy(() => import(/* webpackChunkName: "settings-home" */ './home')); -// const Group = React.lazy(() => import(/* webpackChunkName: "settings-group" */ './group')); - -// const Fallback = () => { -// return ( -// <> -// <Header /> -// <DelayLayout wait={true} type="settings" /> -// </> -// ); -// }; - -// const HomeScreen = (props: NativeStackScreenProps<RouteParams, 'SettingsHome'>) => { -// return ( -// <React.Suspense fallback={<Fallback />}> -// <Home {...props} /> -// </React.Suspense> -// ); -// }; - -// const GroupScreen = (props: NativeStackScreenProps<RouteParams, 'SettingsGroup'>) => { -// return ( -// <React.Suspense fallback={<Fallback />}> -// <Group {...props} /> -// </React.Suspense> -// ); -// }; - export const Settings = () => { const { colors } = useThemeColors(); return ( @@ -62,7 +34,7 @@ export const Settings = () => { screenListeners={{ focus: (e) => { if (e.target?.startsWith("SettingsHome-")) { - useNavigationStore.getState().update({ name: "Settings" }, false); + useNavigationStore.getState().update("Settings"); } } }} diff --git a/apps/mobile/app/screens/tags/index.tsx b/apps/mobile/app/screens/tags/index.tsx index 9a55a0878..048777074 100644 --- a/apps/mobile/app/screens/tags/index.tsx +++ b/apps/mobile/app/screens/tags/index.tsx @@ -18,23 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React from "react"; -import { db } from "../../common/database"; import DelayLayout from "../../components/delay-layout"; +import { Header } from "../../components/header"; import List from "../../components/list"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import SettingsService from "../../services/settings"; import useNavigationStore from "../../stores/use-navigation-store"; import { useTagStore } from "../../stores/use-tag-store"; -const prepareSearch = () => { - SearchService.update({ - placeholder: "Search in tags", - type: "tags", - title: "Tags", - get: () => db.tags?.all - }); -}; +import { db } from "../../common/database"; export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => { const tags = useTagStore((state) => state.tags); @@ -44,11 +36,7 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => { route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name - }); - - SearchService.prepareSearch = prepareSearch; + useNavigationStore.getState().setFocusedRouteId(route.name); return !prev?.current; }, onBlur: () => false, @@ -56,20 +44,36 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => { }); return ( - <DelayLayout> - <List - data={tags} - dataType="tag" - headerTitle="Tags" - loading={!isFocused} - renderedInRoute="Tags" - placeholder={{ - title: "Your tags", - paragraph: "You have not created any tags for your notes yet.", - loading: "Loading your tags." + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={false} + hasSearch={true} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name}`, + type: "tag", + title: route.name, + route: route.name + }); }} /> - </DelayLayout> + <DelayLayout> + <List + data={tags} + dataType="tag" + headerTitle="Tags" + loading={!isFocused} + renderedInRoute="Tags" + placeholder={{ + title: "Your tags", + paragraph: "You have not created any tags for your notes yet.", + loading: "Loading your tags." + }} + /> + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/screens/trash/index.tsx b/apps/mobile/app/screens/trash/index.tsx index 6e0741fee..ef315648d 100644 --- a/apps/mobile/app/screens/trash/index.tsx +++ b/apps/mobile/app/screens/trash/index.tsx @@ -22,22 +22,14 @@ import { db } from "../../common/database"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; import { presentDialog } from "../../components/dialog/functions"; +import { Header } from "../../components/header"; import List from "../../components/list"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { ToastManager } from "../../services/event-manager"; import Navigation, { NavigationProps } from "../../services/navigation"; -import SearchService from "../../services/search"; import useNavigationStore from "../../stores/use-navigation-store"; import { useSelectionStore } from "../../stores/use-selection-store"; import { useTrashStore } from "../../stores/use-trash-store"; -const prepareSearch = () => { - SearchService.update({ - placeholder: "Search in trash", - type: "trash", - title: "Trash", - get: () => db.trash?.all - }); -}; const onPressFloatingButton = () => { presentDialog({ @@ -81,40 +73,53 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => { route.name, Navigation.routeUpdateFunctions[route.name] ); - useNavigationStore.getState().update({ - name: route.name - }); + useNavigationStore.getState().setFocusedRouteId(route.name); if ( !useTrashStore.getState().trash || useTrashStore.getState().trash?.ids?.length === 0 ) { useTrashStore.getState().setTrash(); } - SearchService.prepareSearch = prepareSearch; return false; }, onBlur: () => false }); return ( - <DelayLayout> - <List - data={trash} - dataType="trash" - renderedInRoute="Trash" - loading={!isFocused} - placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())} - headerTitle="Trash" + <> + <Header + renderedInRoute={route.name} + title={route.name} + canGoBack={false} + hasSearch={true} + onSearch={() => { + Navigation.push("Search", { + placeholder: `Type a keyword to search in ${route.name}`, + type: "trash", + title: route.name, + route: route.name + }); + }} /> - - {trash && trash?.ids?.length !== 0 ? ( - <FloatingButton - title="Clear all trash" - onPress={onPressFloatingButton} - alwaysVisible={true} + <DelayLayout> + <List + data={trash} + dataType="trash" + renderedInRoute="Trash" + loading={!isFocused} + placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())} + headerTitle="Trash" /> - ) : null} - </DelayLayout> + + {trash && trash?.ids?.length !== 0 ? ( + <FloatingButton + title="Clear all trash" + onPress={onPressFloatingButton} + alwaysVisible={true} + /> + ) : null} + </DelayLayout> + </> ); }; diff --git a/apps/mobile/app/services/navigation.ts b/apps/mobile/app/services/navigation.ts index 359a5fbfc..9c7184fe0 100755 --- a/apps/mobile/app/services/navigation.ts +++ b/apps/mobile/app/services/navigation.ts @@ -21,7 +21,6 @@ import { StackActions } from "@react-navigation/native"; import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { useFavoriteStore } from "../stores/use-favorite-store"; import useNavigationStore, { - CurrentScreen, GenericRouteParam, RouteName, RouteParams @@ -34,8 +33,8 @@ import { useTrashStore } from "../stores/use-trash-store"; import { eOnNewTopicAdded } from "../utils/events"; import { rootNavigatorRef, tabBarRef } from "../utils/global-refs"; import { eSendEvent } from "./event-manager"; -import SettingsService from "./settings"; import SearchService from "./search"; +import SettingsService from "./settings"; /** * Routes that should be updated on focus @@ -113,59 +112,35 @@ function queueRoutesForUpdate(...routesToUpdate: RouteName[]) { routesToUpdate?.length > 0 ? routesToUpdate : (Object.keys(routeNames) as (keyof RouteParams)[]); - const currentScreen = useNavigationStore.getState().currentScreen; - if (routes.indexOf(currentScreen.name) > -1) { - routeUpdateFunctions[currentScreen.name]?.(); - clearRouteFromQueue(currentScreen.name); + const currentRoute = useNavigationStore.getState().currentRoute; + if (routes.indexOf(currentRoute) > -1) { + routeUpdateFunctions[currentRoute]?.(); + clearRouteFromQueue(currentRoute); // Remove focused screen from queue - routes.splice(routes.indexOf(currentScreen.name), 1); + routes.splice(routes.indexOf(currentRoute), 1); } routesUpdateQueue = routesUpdateQueue.concat(routes); routesUpdateQueue = [...new Set(routesUpdateQueue)]; } -function navigate<T extends RouteName>( - screen: Omit<Partial<CurrentScreen>, "name"> & { - name: keyof RouteParams; - }, - params?: RouteParams[T] -) { - useNavigationStore - .getState() - .update(screen as CurrentScreen, !!params?.canGoBack); - if (screen.name === "Notebook") - routeUpdateFunctions["Notebook"](params || {}); - if (screen.name?.endsWith("Notes") && screen.name !== "Notes") - routeUpdateFunctions[screen.name]?.(params || {}); - //@ts-ignore Not sure how to fix this for now ignore it. - rootNavigatorRef.current?.navigate<RouteName>(screen.name, params); +function navigate<T extends RouteName>(screen: T, params?: RouteParams[T]) { + rootNavigatorRef.current?.navigate(screen as any, params); } function goBack() { rootNavigatorRef.current?.goBack(); } -function push<T extends RouteName>( - screen: CurrentScreen, - params: RouteParams[T] -) { - useNavigationStore.getState().update(screen, !!params?.canGoBack); - rootNavigatorRef.current?.dispatch(StackActions.push(screen.name, params)); +function push<T extends RouteName>(screen: T, params: RouteParams[T]) { + rootNavigatorRef.current?.dispatch(StackActions.push(screen as any, params)); } -function replace<T extends RouteName>( - screen: CurrentScreen, - params: RouteParams[T] -) { - useNavigationStore.getState().update(screen, !!params?.canGoBack); - rootNavigatorRef.current?.dispatch(StackActions.replace(screen.name, params)); +function replace<T extends RouteName>(screen: T, params: RouteParams[T]) { + rootNavigatorRef.current?.dispatch(StackActions.replace(screen, params)); } function popToTop() { rootNavigatorRef.current?.dispatch(StackActions.popToTop()); - useNavigationStore.getState().update({ - name: (SettingsService.get().homepage as RouteName) || "Notes" - }); } function openDrawer() { diff --git a/apps/mobile/app/stores/item-selection-store.ts b/apps/mobile/app/stores/item-selection-store.ts new file mode 100644 index 000000000..13b565392 --- /dev/null +++ b/apps/mobile/app/stores/item-selection-store.ts @@ -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 + }); + } + })); +} diff --git a/apps/mobile/app/stores/use-navigation-store.ts b/apps/mobile/app/stores/use-navigation-store.ts index 5ff30c6ff..fa2b57635 100644 --- a/apps/mobile/app/stores/use-navigation-store.ts +++ b/apps/mobile/app/stores/use-navigation-store.ts @@ -19,17 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { Color, + ItemType, Note, Notebook, Reminder, Tag, - Topic, TrashItem } from "@notesnook/core/dist/types"; import create, { State } from "zustand"; import { ColorValues } from "../utils/colors"; -export type GenericRouteParam = { [name: string]: unknown }; +export type GenericRouteParam = undefined; export type NotebookScreenParams = { item: Notebook; @@ -38,7 +38,7 @@ export type NotebookScreenParams = { }; export type NotesScreenParams = { - item: Note | Notebook | Topic | Tag | Color | TrashItem | Reminder; + item: Note | Notebook | Tag | Color | TrashItem | Reminder; title: string; canGoBack?: boolean; }; @@ -56,90 +56,64 @@ export type AuthParams = { export type RouteParams = { Notes: GenericRouteParam; - Notebooks: GenericRouteParam; + Notebooks: { + canGoBack?: boolean; + }; Notebook: NotebookScreenParams; NotesPage: NotesScreenParams; Tags: GenericRouteParam; Favorites: GenericRouteParam; Trash: GenericRouteParam; - Search: GenericRouteParam; + Search: { + placeholder: string; + type: ItemType; + title: string; + route: RouteName; + ids?: string[]; + }; Settings: GenericRouteParam; TaggedNotes: NotesScreenParams; ColoredNotes: NotesScreenParams; TopicNotes: NotesScreenParams; Monographs: NotesScreenParams; AppLock: AppLockRouteParams; - Auth: AuthParams; Reminders: GenericRouteParam; SettingsGroup: GenericRouteParam; }; export type RouteName = keyof RouteParams; -export type CurrentScreen = { - name: RouteName; - id: string; - title?: string; - type?: string; - color?: string | null; - notebookId?: string; - beta?: boolean; -}; - export type HeaderRightButton = { title: string; onPress: () => void; }; interface NavigationStore extends State { - currentScreen: CurrentScreen; - currentScreenRaw: Partial<CurrentScreen>; + currentRoute: RouteName; canGoBack?: boolean; - update: ( - currentScreen: Omit<Partial<CurrentScreen>, "name"> & { - name: keyof RouteParams; - }, - canGoBack?: boolean, - headerRightButtons?: HeaderRightButton[] - ) => void; + focusedRouteId?: string; + update: (currentScreen: RouteName) => void; headerRightButtons?: HeaderRightButton[]; buttonAction: () => void; setButtonAction: (buttonAction: () => void) => void; + setFocusedRouteId: (id?: string) => void; } const useNavigationStore = create<NavigationStore>((set, get) => ({ - currentScreen: { - name: "Notes", - id: "notes_navigation", - title: "Notes", - type: "notes" - }, - currentScreenRaw: { name: "Notes" }, - canGoBack: false, - update: (currentScreen, canGoBack, headerRightButtons) => { - const color = - ColorValues[ - currentScreen.color?.toLowerCase() as keyof typeof ColorValues - ]; - if ( - JSON.stringify(currentScreen) === JSON.stringify(get().currentScreenRaw) - ) - return; + focusedRouteId: "Notes", + setFocusedRouteId: (id) => { set({ - currentScreen: { - name: currentScreen.name, - id: - currentScreen.id || currentScreen.name.toLowerCase() + "_navigation", - title: currentScreen.title || currentScreen.name, - type: currentScreen.type, - color: color, - notebookId: currentScreen.notebookId, - beta: currentScreen.beta - }, - currentScreenRaw: currentScreen, - canGoBack, - headerRightButtons: headerRightButtons + focusedRouteId: id }); + console.log("CurrentRoute ID", id); + }, + currentRoute: "Notes", + canGoBack: false, + update: (currentScreen) => { + set({ + currentRoute: currentScreen + }); + console.log("CurrentRoute", currentScreen); }, headerRightButtons: [], buttonAction: () => null, diff --git a/apps/theme-builder/package-lock.json b/apps/theme-builder/package-lock.json index 71076db96..f16c44b54 100644 --- a/apps/theme-builder/package-lock.json +++ b/apps/theme-builder/package-lock.json @@ -1404,6 +1404,7 @@ "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0", "@tanstack/react-query": "^4.29.19", + "@tanstack/react-virtual": "^3.0.0-beta.68", "@theme-ui/color": "^0.14.7", "@theme-ui/components": "^0.14.7", "@theme-ui/core": "^0.14.7", @@ -1439,7 +1440,6 @@ "react-modal": "3.13.1", "react-qrcode-logo": "^2.2.1", "react-scroll-sync": "^0.9.0", - "react-virtuoso": "^4.4.2", "timeago.js": "4.0.2", "tinycolor2": "^1.6.0", "w3c-keyname": "^2.2.6",